临时内核映射与永久内核映射的区别是,临时内核映射可以在中断处理程序和可延迟函数内部使用,它不堵塞当前进程。
一 原理介绍
临时内核映射的线性地址在永久内核映射的后面,范围是[FIXADDR_START, FIXADDR_TOP),其基本逻辑是获取一个计数值n,用FIXADDR_TOP - n*4k 得到映射的线性地址。n不同,得到的线性地址也就不相同,内核是如何确保不同线程得到的n值不同的呢。
有两个措施:
1. 每cpu变量__kmap_atomic_idx (见kmap_atomic_idx_push函数)
对于每cpu变量__kmap_atomic_idx,每个cpu都维护着一个计数,比如cpu1保存的计数是3,而cpu2保存的计数是5。映射高端内存时,将计数加1,取消映射时,将计数减1。
假如一个线程运行在cpu1上,映射内存时计数加1,__kmap_atomic_idx的值变为4;另一个线程运行在cpu2上,映射内存后计数加1,__kmap_atomic_idx的值为6。
2. 每个cpu有KM_TYPE_NR个固定映射的线性地址
KM_TYPE_NR的值为20,即[0, 20),为第一个cpu的取值范围,[20, 40)为第二个cpu的取值范围,以此类推。
idx = type + KM_TYPE_NR*smp_processor_id();
上面的逻辑可以参考下图
图1
二 临时内核映射代码
有了上面的介绍后,下面看下临时内核映射的代码,kmap_atomic
static inline int kmap_atomic_idx_push(void)
{
int idx = __this_cpu_inc_return(__kmap_atomic_idx) - 1;
return idx;
}
void *kmap_atomic_prot(struct page *page, pgprot_t prot)
{
unsigned long vaddr;
int idx, type;
preempt_disable();
pagefault_disable();
// 非高端内存,直接返回其线性地址
if (!PageHighMem(page))
return page_address(page);
// 获取每cpu变量__kmap_atomic_idx的值,并将值加1
type = kmap_atomic_idx_push();
idx = type + KM_TYPE_NR*smp_processor_id();
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
BUG_ON(!pte_none(*(kmap_pte-idx)));
// 将页框添加到页表中
set_pte(kmap_pte-idx, mk_pte(page, prot));
arch_flush_lazy_mmu_mode();
return (void *)vaddr;
}
void *kmap_atomic(struct page *page)
{
return kmap_atomic_prot(page, kmap_prot);
}
kmap_atomic_prot中的idx即经过前面提到的两个措施后,得到的值。
获取线性地址vaddr时,FIX_KMAP_BEGIN是个枚举值,可参考enum fixed_addresses。
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
三 __fix_to_virt
__fix_to_virt的作用是将固定映射的一个值转成线性地址,其定义如下:
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
32位系统地址空间如下图所示:
图2
对于32位系统,FIXADDR_TOP为0xFFFFF000,即地址空间最后一个4K的起始地址
对于64位系统,FIXADDR_TOP为0xFFFFFFFF FF600000,也在线性地址空间最后的位置
x为0时, __fix_to_virt得到临时内核映射线性区最后一页的线性地址;x为1时,得到倒数第二页的线性地址,以此类推。
四 kmap_pte
kmap_pte的作用,与永久内核映射中的pkmap_page_table作用的一样的,也是指向一个页表。与pkmap_page_table不同的是,kmap_pte指向的是临时映射所用页表的结束位置。
set_pte(kmap_pte-idx, mk_pte(page, prot));
kmap_pte在kmap_init中初始化
static void __init kmap_init(void)
{
unsigned long kmap_vstart;
/*
* Cache the first kmap pte:
*/
kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);
kmap_pte = kmap_get_fixmap_pte(kmap_vstart);
}
图3
kmap_vstart距离FIXADDR_TOP有 FIX_KMAP_BEGIN * 4K的,如上图所示,kmap_pte为kmap_vstart所在的页表。
五 kmap_get_fixmap_pte
调用kmap_get_fixmap_pte将kmap_vstart转换成kmap_pte。
static inline pte_t *kmap_get_fixmap_pte(unsigned long vaddr)
{
pgd_t *pgd = pgd_offset_k(vaddr); // 页全局目录
p4d_t *p4d = p4d_offset(pgd, vaddr);
pud_t *pud = pud_offset(p4d, vaddr); // 页上级目录
pmd_t *pmd = pmd_offset(pud, vaddr); // 页中间目录
return pte_offset_kernel(pmd, vaddr); // 页表
}
六 kmap_atomic在内核中的应用
在一个函数中只调用一次kmap_atomic,如下所示:
void read_inline_data(struct page *page, struct page *ipage)
{
struct inode *inode = page->mapping->host;
void *src_addr, *dst_addr;
if (PageUptodate(page))
return;
f2fs_bug_on(F2FS_P_SB(page), page->index);
zero_user_segment(page, MAX_INLINE_DATA(inode), PAGE_SIZE);
/* Copy the whole inline data block */
src_addr = inline_data_addr(inode, ipage);
// 映射
dst_addr = kmap_atomic(page);
memcpy(dst_addr, src_addr, MAX_INLINE_DATA(inode));
flush_dcache_page(page);
// 取消映射
kunmap_atomic(dst_addr);
if (!PageUptodate(page))
SetPageUptodate(page);
}
映射后得到线性地址dst_addr,对线性地址进行操作,最后取消映射。
在一个函数中调用多次 kmap_atomic,看下面代码:
void copy_user_highpage(struct page *to, struct page *from,
unsigned long u_vaddr, struct vm_area_struct *vma)
{
void *kfrom = kmap_atomic(from);
void *kto = kmap_atomic(to);
int clean_src_k_mappings = 0;
... ...
kunmap_atomic(kto);
kunmap_atomic(kfrom);
}
先映射from页框得到线性地址kfrom,再映射页框to得到线性地址kto。每调用一次kmap_atomic,运行此段代码的cpu保存的__kmap_atomic_idx就加1,因此最后得到的线性地址kfrom和ktto就不相同。
线性地址使用完毕后,调用kunmap_atomic取消映射,对应cpu保存的__kmap_atomic_idx的值也减1。