目录
kunmap_high(struct page *page)函数实现
说明:
- kernel版本:5.9.8
- 调试工具:gdb + qemu + kernel-O0
- 平台信息:ARM Vexpress ARMv7 4核 SMP
- 使用工具:vim + cscope + visio
- vmap()用于相对长时间的映射,可同时映射多个pages,
- kmap()和kmap_atomic()则用于相对短时间的映射,只能映射单个page.
- 而 ioremap 函数,其功能是将给定的物理地址映射为虚拟地址。
注意此处的物理地址并不是真正内存的物理地址,而是cpu上的io memory。
本篇文章重点从以下方面分析:
- 1、散列表
- 2、kmap
- 3、vmap
- 4、ioremap
散列表也是哈希表
散列表:根据给定的关键字来计算关键字在表中的地址的数据结构,也就说,散列表建立了关键字和存储地址之间的一种直接映射关系。
散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为Hash(key)=Addr
冲突:散列函数可能会把两个或者两个以上不同关键字映射同一地址,称这种情况为冲突,这些发生碰撞的不同关键字称为同义词。
构造散列函数的tips:
- 1、散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围
- 2、散列函数计算出来的地址应该能等概率,均分布在整个地址空间,从而减少冲突的发生。
- 3、散列函数应该尽量简单,能够在较短的时间内就计算出任一关键字对应的散列地址
常用Hash函数的构造方法:
- 1、直接定址法
- 2、除留余数法
- 3、数字分析法
- 4、平方取中法
kmap实现
用于相对短时间的映射,只能映射单个page.
page_address_map
struct page_address_map {
struct page *page;
void *virtual;
struct list_head list;
};
struct page_address_map:散列表中关键字(key=page)对应的结构,散列表中的结点
- page:管理一个高端的物理page
- virtual:page物理页面对应的虚拟地址
- list:链接到page_address_htable[i]对应lh的头部的结点
pkmap_count
#define PTRS_PER_PTE 512
#define LAST_PKMAP PTRS_PER_PTE
static int pkmap_count[LAST_PKMAP]
- 1、pkmap_count[i]=0为空闲的虚拟地址可以直接使用的
- 2、pkmap_count[i]=1已经被使用过且被释放了但是还在缓存中
- 3、pkmap_count[i]>=2正在使用中
- 4、pkmap_count[i]描述的虚拟地址为0xbfe00000+4k*i
来一张图描述pkmap_count和虚拟地址之间的对应关系
page_address_slot
static struct page_address_slot {
struct list_head lh; /* List of page_address_maps */
spinlock_t lock; /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];
- page_address_slot:散列表的描述结构,page_address_htable为散列表
- lh:链接哈希表中的成员结点,此哈希表通过链表解决冲突问题
哈希函数
#define hash_long(val, bits) hash_32(val, bits)
#define hash_32 hash_32_generic
#define __hash_32 __hash_32_generic
#define PA_HASH_ORDER 7
#define GOLDEN_RATIO_32 0x61C88647
static inline u32 __hash_32_generic(u32 val)
{
return val * GOLDEN_RATIO_32;
}
static inline u32 hash_32_generic(u32 val, unsigned int bits)
{
/* High bits are more random, so use them. */
return __hash_32(val) >> (32 - bits);
}
static inline u32 hash_ptr(const void *ptr, unsigned int bits)
{
return hash_long((unsigned long)ptr, bits);
}
static struct page_address_slot *page_slot(const struct page *page)
{
return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}
哈希函数通过以上的各种转换之后的表达式为:
Hash(page)=page*GOLDEN_RATIO_32>>(32-PA_HASH_ORDER)
哈希函数的选择遵循构造散列函数的tips
再来一张图描述page_address_slot和page_address_map之间的关系
kmap函数实现
page如果是ZONE_NORMAL,则直接返回虚拟地址即可。
page如果是ZONE_HIGHMEM,则分以下情况:
- 1、如果page在page_address_htable中直接返回虚拟地址
- 2、如果page不在page_address_htable中,则分以下情况:
- 如果只有pkmap_count[0]为0,则会进行一次flush_all_zero_pkmaps(),然后直接退出,因为pkmap_count[0]为0,则直接返回pkmap_count[0]对应的虚拟地址。
- 如果有pkmap_count[i]的数组为0,i非0,i的下标最小,则直接拿到pkmap_count[i]对应的虚拟地址。
- 如果全部非0,则分以下情况
- 1、有可以的释放的,则通过flush_all_zero_pkmaps()释放,先判断pkmap_count[0]释放释放满足要求,如果不满足要求,然后再循环一次判断,发现有空闲的,则获取对应的虚拟地址。
- 2、如果没有可以释放的,即都是在使用的,则需要加入等待队列,直到有空闲的释放被唤醒。
kmap_init
static void __init kmap_init(void)
{
#ifdef CONFIG_HIGHMEM
pkmap_page_table = early_pte_alloc(pmd_off_k(PKMAP_BASE),
PKMAP_BASE, _PAGE_KERNEL_TABLE);
#endif
}
从上可以看出上面对[KMAP_BASE,PAGE_OFFSET]进行PGD页表的填充,页表的port为_PAGE_KERNEL_TABLE,虚拟地址为FIXADDR_START,early_pte_alloc通过early_alloc函数pte_t*pte = alloc (PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE)申请 512 * 4 + 512 *4其中硬件页表和软件各占一半,512硬件PTE表项,一个PTE表项对应4k,刚好2MB对应[KMAP_BASE,PAGE_OFFSET]大小,初始化完成之后,一级页表PGD表项由KMAP_BASE起始地址的PGD表项(PGD[0],PGD[1])指向对应pte的基地址。此后如果在[KMAP_BASE,PAGE_OFFSET]要映射对应的物理地址,只需要填充PTE表项即可。
kmap
static inline void *kmap(struct page *page)
{
void *addr;
might_sleep();
if (!PageHighMem(page))
addr = page_address(page);
else
addr = kmap_high(page);
kmap_flush_tlb((unsigned long)addr);
return addr;
}
1、判断page页面是否是ZONE_HIGHMEM,如果不是,则认为是ZONE_NORMAL,因为ZONE_NORMAL为低端ZONE,则为线性映射,直接获取虚拟addr通过lowmem_page_address函数
2、如果给是page指向ZONE_HIGHMEM的物理页面,需要kamp_high做物理地址和虚拟地址的映射
3、获取到映射完成的虚拟地址之后刷TLB后,返回虚拟地址
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;
}
- 1、已经确定page是ZONE_HIGHMEM,通过page_address函数的hash表找到对应的虚拟地址,如果虚拟地址返回为NULL,则重新进行映射
- 2、对pkmap_count[PKMAP_NR(vaddr)]++到2,说明此page对应虚拟地址已经被分配出去使用了
- 3、返回映射成功的虚拟地址。
接下来重点分析page_address函数
page_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;
}
- 1、此page已经确定是ZONE_HIGHMEM,则直接通过page_slot(page)函数(哈希函数)获取到此page的储存器地址
- 2、因为page可能产生冲突,则需要通过lh链表头遍历此page是否在此链表中,如果存在则直接返回虚拟地址即可,说明之前此page已经建立好了映射,虚拟地址直接使用即可,如果未找到,则需要通过map_new_virtual(page)函数重新映射。
map_new_virtual
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;
{
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;
}
static inline int no_more_pkmaps(unsigned int pkmap_nr, unsigned int color)
{
return pkmap_nr == 0;
}
static inline unsigned int get_pkmap_color(struct page *page)
{
return 0;
}
static inline int get_pkmap_entries_count(unsigned int color)
{
return LAST_PKMAP;
}
static inline unsigned int get_next_pkmap_nr(unsigned int color)
{
static unsigned int last_pkmap_nr;
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
return last_pkmap_nr;
}
get_pkmap_color(page)函数仅在特定体系结构(mips和xtensa)中使用,并且由于未在ARM中使用而返回0
color=0,count=512,last_pkmap_nr=1开始循环遍历pkmap_count [last_pkmap_nr]是否为0,如果为0,说明此PTE还未填充或者PTE无效。现在可以拿来用填充,如果512个表项全为非0,即所有的PTE都映射了page,即已经经过kmap和kumap,或者在kunmap中,但是没有实际的解映射,只是pkmap_count[last_pkmap_nr]--。此page对应的物理地址和对应的虚拟地址通过hashtable存储。
count=512,last_pkmap_nr=1
count=511,last_pkmap_nr=2
…
count=1,last_pkmap_nr=512
然后此时if(no_more_pkmaps(last_pkmap_nr, color))成立
- 1、先flush_cache_kmaps()
- 2、循环遍历pkmap_count[i],如果等于1,则说明page已经kumap了,把pkmap_count[i] = 0,然后通过pte_clear清空pte和物理地址的页表的映射关系
- 3、然后通过set_page_address(page, NULL)函数把page从hashtable删除,同时need_flush置1,然后延时刷TLB
然后再判断pkmap_count[0]是否满足要求,如果不满足再循环检测是否有空闲的pkmap_count[last_pkmap_nr]
count=512 last_pkmap_nr=0;
count=511 last_pkmap_nr=1;
count=510 last_pkmap_nr=2;
count=1 last_pkmap_nr=511
如果count=0,还是无法找到空闲的pkmap_count[last_pkmap_nr]
则把先创建一个等待队列,然后把此任务加入到等待队列中,同时把此任务设置为不可屏蔽中断状态,等待唤醒,如果有其他任务释放了pkmap_count[last_pkmap_nr],即是把pkmap_count[last_pkmap_nr]设置为1了,会wakeup此任务,然后从等待队列中删除此任务,然后先尝试在哈希表中是否能找到对应的page,如果找不到,需要重新start即是上述流程。<为何需要先page_address,有可能同一个page在其他高优先级的进程kmap了也没拿到,但是等有空闲得到时候,高优先级任务可能优先拿到,占了一个pkmap_count,低优先级的任务也要占一个pkmap_count,导致同一个page占了两个pkmap_count>
- 4、通过last_pkmap_nr找到对应的虚拟地址,然后用通过set_pte_at函数填充PTE页表建立映射。
- 5、把pkmap_count[last_pkmap_nr] = 1表示此PTE已经被填充
- 6、通过set_page_address(page, (void *)vaddr)函数把page加入哈希表中
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) { /* 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;
}
- 1、通过page找到对应存储page_address_map的地址
- 2、如果虚拟地址非NULL,则说明是add,如果为空则为remove
- 3、如果是add的话即先通过虚拟地址找到对应的page_address_maps[PKMAP_NR((unsigned long)virtual)]然后更新page_address_map中的page和virtual,通过page_address_map的list添加到page_address_slot中的lh的头部中list_add_tail(&pam->list, &pas->lh);
- 4、如果是remove的话,找到对应存储地址,然后遍历page_address_slot中的page和当前page是否相等,如果相等就直接list_del
kunmap
释放过程
- 1、page如果是ZONE_NORMAL则直接返回
- 2、page如果是ZONE_HIGHMEM, pkmap_count[nr],然后判断等待队列是否为空,如果非空,则唤醒等待队列中的任务。
static inline void kunmap(struct page *page)
{
might_sleep();
if (!PageHighMem(page))
return;
kunmap_high(page);
}
- 1、如果是指向ZONE_NORMAL的page,则直接返回
- 2、如果是指向ZONE_HIGHMEM的page ,则调用 kunmap_high(page)
kunmap_high(struct page *page)函数实现
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);
}
- 1、通过get_pkmap_color(page)函数获取color=0,--pkmap_count[nr]表示释放
- 2、通过page找到vaddr地址,如果释放不存在的page则BUG_ON,然后通过PKMAP_NR(addr)找到数组下标nr
- 3、通过get_pkmap_wait_queue_head(color)获取等待队列的头,然后查看等待队列中是否有任务需要唤醒,如果有则把need_wakeup稍后去唤醒,如果空,则无需唤醒。
vmap实现
vmap()用于相对长时间的映射,可同时映射多个pages,
vmap函数
void *vmap(struct page **pages, unsigned int count,
unsigned long flags, pgprot_t prot)
{
struct vm_struct *area;
unsigned long size; /* In bytes */
might_sleep();
if (count > totalram_pages())
return NULL;
size = (unsigned long)count << PAGE_SHIFT;
area = get_vm_area_caller(size, flags, __builtin_return_address(0));
if (!area)
return NULL;
if (map_kernel_range((unsigned long)area->addr, size, pgprot_nx(prot),
pages) < 0) {
vunmap(area->addr);
return NULL;
}
return area->addr;
}
1、先在vmalloc区域获取size大小的虚拟地址
2、找到虚拟地址后通过map_kernel_range函数把虚拟地址和物理地址绑定映射,即是填充PGD和PTE页表
vunmap函数
void vunmap(const void *addr)
{
BUG_ON(in_interrupt());
might_sleep();
if (addr)
__vunmap(addr, 0);
}
1、如果在中断上下文,直接BUG_ON
2、然后通过__vunmap函数释放,因为第二个参数为0,即不释放对应物理地址对应的物理page
ioremap
ioremap 函数,其功能是将给定的物理地址映射为虚拟地址。
ioremap函数
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{
return arch_ioremap_caller(res_cookie, size, MT_DEVICE,
__builtin_return_address(0));
}
void __iomem * (*arch_ioremap_caller)(phys_addr_t, size_t,
unsigned int, void *) =
__arm_ioremap_caller;
void __iomem *__arm_ioremap_caller(phys_addr_t phys_addr, size_t size,
unsigned int mtype, void *caller)
{
phys_addr_t last_addr;
unsigned long offset = phys_addr & ~PAGE_MASK;
unsigned long pfn = __phys_to_pfn(phys_addr);
/*
* Don't allow wraparound or zero size
*/
last_addr = phys_addr + size - 1;
if (!size || last_addr < phys_addr)
return NULL;
return __arm_ioremap_pfn_caller(pfn, offset, size, mtype,
caller);
}
- 1、phys_addr找到对应物理地址的页内偏移
- 2、由phys_addr找到物理页帧号
- 3、做些简单参数检查size非0及环绕
__arm_ioremap_pfn_caller函数
static void __iomem * __arm_ioremap_pfn_caller(unsigned long pfn,
unsigned long offset, size_t size, unsigned int mtype, void *caller)
{
const struct mem_type *type;
int err;
unsigned long addr;
struct vm_struct *area;
phys_addr_t paddr = __pfn_to_phys(pfn);
#ifndef CONFIG_ARM_LPAE
/*
* High mappings must be supersection aligned
*/
if (pfn >= 0x100000 && (paddr & ~SUPERSECTION_MASK))
return NULL;
#endif
type = get_mem_type(mtype);
if (!type)
return NULL;
/*
* Page align the mapping size, taking account of any offset.
*/
size = PAGE_ALIGN(offset + size);
/*
* Try to reuse one of the static mapping whenever possible.
*/
if (size && !(sizeof(phys_addr_t) == 4 && pfn >= 0x100000)) {
struct static_vm *svm;
svm = find_static_vm_paddr(paddr, size, mtype);
if (svm) {
addr = (unsigned long)svm->vm.addr;
addr += paddr - svm->vm.phys_addr;
return (void __iomem *) (offset + addr);
}
}
/*
* Don't allow RAM to be mapped with mismatched attributes - this
* causes problems with ARMv6+
*/
if (WARN_ON(pfn_valid(pfn) && mtype != MT_MEMORY_RW))
return NULL;
area = get_vm_area_caller(size, VM_IOREMAP, caller);
if (!area)
return NULL;
addr = (unsigned long)area->addr;
area->phys_addr = paddr;
err = ioremap_page_range(addr, addr + size, paddr,
__pgprot(type->prot_pte));
if (err) {
vunmap((void *)addr);
return NULL;
}
flush_cache_vmap(addr, addr + size);
return (void __iomem *) (offset + addr);
}
- 1、由pfn获取物理地址
- 2、如果物理地址大于4G则需要16MB对齐
- 3、由get_mem_type(mtype)函数获取内存类型,设置页表权限
[MT_DEVICE] = { /* Strongly ordered / ARMv6 shareddevice */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED |
L_PTE_SHARED,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE | PMD_SECT_S,
.domain = DOMAIN_IO,
},
- 4、如果页内偏移为0xff0,size为2,则只需要4k虚拟地址即可,如果size为0x10,则需要两4k大小的虚拟地址映射。
- 5、无static mapping暂不分析
- 6、mtype !=MT_MEMORY_RW成立,pfn_valid函数稍后重点分析
- 7、通过get_vm_area_caller函数获取虚拟地址
- 8、通过虚拟地址和物理地址及页表属性建立映射,填充PGD PTE页表。
- 9、flush_cache_vmap刷cachevmap
pfn_valid(pfn)
int pfn_valid(unsigned long pfn)
{
phys_addr_t addr = __pfn_to_phys(pfn);
if (__phys_to_pfn(addr) != pfn)
return 0;
return memblock_is_map_memory(addr);
}
bool __init_memblock memblock_is_map_memory(phys_addr_t addr)
{
int i = memblock_search(&memblock.memory, addr);
if (i == -1)
return false;
return !memblock_is_nomap(&memblock.memory.regions[i]);
}
static int __init_memblock memblock_search(struct memblock_type *type, phys_addr_t addr)
{
unsigned int left = 0, right = type->cnt;
do {
unsigned int mid = (right + left) / 2;
if (addr < type->regions[mid].base)
right = mid;
else if (addr >= (type->regions[mid].base +
type->regions[mid].size))
left = mid + 1;
else
return mid;
} while (left < right);
return -1;
}
static inline bool memblock_is_nomap(struct memblock_region *m)
{
return m->flags & MEMBLOCK_NOMAP;
}
- 1、由pfn找到物理地址
- 2、memblock_is_map_memory由此函数判断memory是否是map区域
如果在memblock.memory区域找到了,说明是memblock.memory区域,否则不是memory区域返回false=0,即WARN_ON不执行异常,如果是memblock.memory区域则需要判断此memblock是否是MEMBLOCK_NOMAP属性,如果是WARN_ON不执行异常,如果不是则触发异常警告。
void (*arch_iounmap)(volatile void __iomem *) = __iounmap;
void iounmap(volatile void __iomem *cookie)
{
arch_iounmap(cookie);
}
void __iounmap(volatile void __iomem *io_addr)
{
void *addr = (void *)(PAGE_MASK & (unsigned long)io_addr);
struct static_vm *svm;
/* If this is a static mapping, we must leave it alone */
svm = find_static_vm_vaddr(addr);
if (svm)
return;
vunmap(addr);
}
- 1、无static_vm直接vunmap(addr)
- 2、如果在中断上下文,直接BUG_ON
- 3、然后通过__vunmap函数释放,因为第二个参数为0,即不释放对应物理地址对应的物理page
后记
本文重点分析了kmap vmap ioreamp的实现,其中vmap,ioremap中的部分函数在vmalloc的实现中已经分析过。对于kmap vmap ioremap的差异对比和使用场景这里暂未仔细做分析,因为如果你知道它的实现细节了,它的使用注意事项也就了然于胸了。从对内存管理的刚开始的懵懂到现在有种面朝大海春暖花开的感觉。