将内核页映射到多个内核虚拟地址上
我在一个针对AArch64的 Linux内核模块上工作,我想在内核中为一个已存在的内存区域创建 别名:也就是说,将第二个(或第三、第四等)内核虚拟地址范围映射到已分配的内存。
我想实现的逻辑是:
- 分配一个较大的虚拟内存数组(1024页)不分配物理页 - 只保留虚拟地址空间:
struct vm_struct *area = get_vm_area_fn(1024*PAGE_SIZE, VM_ALLOC);
2.分配 4个物理页:
struct page *frames[4];
for (int i = 0; i < 4; i++)
frames[i] = alloc_page(GFP_KERNEL);
3.以 轮循 方式把大数组中的每一页映射到一个物理帧:
for (int i = 0; i < 1024; i++)
map_one_page((uintptr_t)area->addr + i*PAGE_SIZE, frames[i % 4]);
经过此步骤,我预计这1024页的数组将以每4 页为一组呈现 别名化,也就是说每4 页共享同一块物理内存。
但当我实现映射函数如下时:
static int map_one_page(unsigned long va, struct page *page) {
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
int err = 0;
pgd = pgd_offset(init_mm_ptr, va);
if (pgd_none(*pgd))
return -ENOMEM;
err = __p4d_alloc_fn(init_mm_ptr, pgd, va);
if (err)
return err;
p4d = p4d_offset(pgd, va);
err = __pud_alloc_fn(init_mm_ptr, p4d, va);
if (err)
return err;
pud = pud_offset(p4d, va);
err = __pmd_alloc_fn(init_mm_ptr, pud, va);
if (err)
return err;
pmd = pmd_offset(pud, va);
err = __pte_alloc_kernel_fn(pmd, va);
if (err)
return err;
pte = pte_offset_kernel(pmd, va);
/* Build PTE */
pte_t entry = pfn_pte(page_to_pfn(page), PAGE_KERNEL);
/* Install */
set_pte_at(init_mm_ptr, va, pte, entry);
flush_tlb_kernel_range(va, va + PAGE_SIZE);
return 0;
}
其中init_mm_ptr等于 &init_mm,其他所有 _fn 函数都是指向内核函数且名称相应的指针(我这样做是因为出于某些原因无法直接调用它们)。
实现上述之后,我先尝试写入 ((char)*v->addr)[0],紧接着再写入 ((char)v->addr)[PAGE_SIZE4],然后打印这两个地址,期望在两种情形下都打印出第二个值,但实际打印出的分别是各自的值(也就是说它们并未发生别名)。
解决方案
我已经成功让它工作,如下所示:
#define NUM_PHYS_PAGES 4
#define NUM_VA_PAGES 128
static struct page** pages = NULL;
struct page *phys_pages[NUM_PHYS_PAGES] = { 0 };
unsigned long base_va = 0;
static int __init alias_mapping_init(void) {
int i, ret = 0;
size_t size = NUM_VA_PAGES * PAGE_SIZE;
for (i = 0; i < NUM_PHYS_PAGES; ++i) {
phys_pages[i] = alloc_page(GFP_KERNEL);
if (NULL == phys_pages[i]) {
pr_err("[alias] alloc_page[%d] failed\n", i);
ret = -ENOMEM;
goto err_free_phys;
}
clear_highpage(phys_pages[i]);
}
pages = kmalloc_array(NUM_VA_PAGES, sizeof(*pages), GFP_KERNEL);
if (NULL == pages) {
pr_err("[alias] kmalloc_array failed\n");
ret = -ENOMEM;
goto err_free_phys;
}
for (int j = 0; j < NUM_VA_PAGES; ++j) {
pages[j] = phys_pages[j % NUM_PHYS_PAGES];
}
base_va = (unsigned long)vm_map_ram(pages, NUM_VA_PAGES, -1);
if (!base_va) {
pr_err("[alias] vm_map_ram failed\n");
ret = -ENOMEM;
goto err_free_pages_array;
}
pr_info("[alias] mapping complete: %lx - %lx\n", base_va, base_va + size);
{
char *ptr = (char*)base_va;
pr_info("[alias] initial: p0=%x p4=%x p1=%x\n",
ptr[0],
ptr[PAGE_SIZE * 4],
ptr[PAGE_SIZE * 1]);
ptr[0] = 0xAA;
ptr[PAGE_SIZE * 4] = 0xCC;
ptr[PAGE_SIZE * 1] = 0xBB;
dsb(ish);
isb();
pr_info("[alias] after: p0=%x p4=%x (alias) p1=%x (independent)\n",
ptr[0],
ptr[PAGE_SIZE * 4],
ptr[PAGE_SIZE * 1]);
}
return 0;
err_free_pages_array:
kfree(pages);
pages = NULL;
err_free_phys:
while (--i >= 0) {
__free_page(phys_pages[i]);
}
return ret;
}
static void __exit alias_mapping_exit(void) {
pr_info("[alias] exit\n");
if (base_va) {
vm_unmap_ram((void *)base_va, NUM_VA_PAGES);
base_va = 0;
}
kfree(pages);
pages = NULL;
for (int i = 0; i < NUM_PHYS_PAGES; ++i) {
if (phys_pages[i]) {
__free_page(phys_pages[i]);
phys_pages[i] = NULL;
}
}
}
实际而言,我使用的是 vm_map_ram API,它接收描述我想要的映射的 pages 参数。
如果有人发现问题,当然我也很乐意知道。
供后代参考:vmap 也可以替代 vm_map_ram。