前言
呵呵 这是 linux 中内存管理中很基础的一环
用户程序 操作的地址都是虚拟地址, 虚拟地址通过 mmu 转换为物理地址
用户程序 看到的地址都是一个完整的世界, 只有具体需要使用的时候 产生缺页中断, 然后 分配具体的物理页
这里 要说的就是 虚拟地址 到 物理地址 的转换
体验一下 虚拟地址 转换 为物理地址
主要是来自于 内核模块 来体验, 测试的 模块代码 来自于如下链接
Linux内核学习3——虚拟地址转换成物理地址z
执行的容器是 centos 7.x 桌面版本, 原来的 模块代码 做了一定的调整
#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/mm_types.h>
#include <linux/sched.h>
#include <linux/export.h>
#include <linux/delay.h>
static unsigned long cr0,cr3;
static unsigned long vaddr = 0;
static void get_pgtable_macro(void) //打印页机制中的一些重要参数
{
cr0 = read_cr0();
cr3 = read_cr3_pa();
printk("cr0 = 0x%lx, cr3 = 0x%lx\n",cr0,cr3);
//这些宏是用来指示线性地址中相应字段所能映射的区域大小的对数的
printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
// printk("P4D_SHIFT = %d\n",P4D_SHIFT);
printk("PUD_SHIFT = %d\n", PUD_SHIFT);
printk("PMD_SHIFT = %d\n", PMD_SHIFT);
printk("PAGE_SHIFT = %d\n", PAGE_SHIFT); //指示page offset字段,映射的是一个页面的大小,一个页面大小是4k,转换成以2为底的对数就是12,其他的宏类似
//下面的这些宏是用来指示相应的页目录表中的项的个数的,这些宏都是为了方便寻页时进行位运算的
printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
// printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);
printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);
printk("PAGE_MASK = 0x%lx\n", PAGE_MASK); //page_mask,页内偏移掩码,用来屏蔽掉page offset字段
}
static unsigned long vaddr2paddr(unsigned long vaddr) //线性地址到物理地址转换
{
//首先为每个目录项创建一个变量将它们保存起来
pgd_t *pgd;
// p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
unsigned long paddr = 0;
unsigned long page_addr = 0;
unsigned long page_offset = 0;
pgd = pgd_offset(current->mm,vaddr); //第一个参数是当前进程的mm_struct结构(我们申请的线性地址空间是内核,所以应该查内核页表,又因为所有的进程都共享同一个内核页表,所以可以用当前进程的mm_struct结构来进行查找)
printk("pgd_addr = 0x%lx, *pgd = 0x%lx, pgd_page_addr = 0x%lx, pgd_val = 0x%lx, pgd_index = %lu\n", pgd, *pgd, pgd_page_vaddr(*pgd), pgd_val(*pgd),pgd_index(vaddr));
if (pgd_none(*pgd)){
printk("not mapped in pgd\n");
return -1;
}
// p4d = p4d_offset(pgd, vaddr); //查找到的页全局目录项pgd作为下级查找的参数传入到p4d_offset中
// printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d),p4d_index(vaddr));
// if(p4d_none(*p4d))
// {
// printk("not mapped in p4d\n");
// return -1;
// }
pud = pud_offset(pgd, vaddr);
printk("pud_addr = 0x%lx, *pud = 0x%lx, pud_page_addr = 0x%lx, pud_val = 0x%lx, pud_index = %lu\n", pud, *pud, pud_page_vaddr(*pud), pud_val(*pud),pud_index(vaddr));
if (pud_none(*pud)) {
printk("not mapped in pud\n");
return -1;
}
pmd = pmd_offset(pud, vaddr);
printk("pmd_addr = 0x%lx, *pmd = 0x%lx, pmd_page_addr = 0x%lx, pmd_val = 0x%lx, pmd_index = %lu\n", pmd, *pmd, pmd_page_vaddr(*pmd), pmd_val(*pmd),pmd_index(vaddr));
if (pmd_none(*pmd)) {
printk("not mapped in pmd\n");
return -1;
}
pte = pte_offset_kernel(pmd, vaddr); //与上面略有不同,这里表示在内核页表中查找,在进程页表中查找是另外一个完全不同的函数 这里最后取得了页表的线性地址
printk("pte_addr = 0x%lx, *pte = 0x%lx, pte_val = 0x%lx, ptd_index = %lu\n", pte, *pte, pte_val(*pte),pte_index(vaddr));
if (pte_none(*pte)) {
printk("not mapped in pte\n");
return -1;
}
//从页表的线性地址中取出该页表所映射页框的物理地址
page_addr = pte_val(*pte) & PAGE_MASK; //取出其高48位
//取出页偏移地址,页偏移量也就是线性地址中的低12位
page_offset = vaddr & ~PAGE_MASK;
//将两个地址拼接起来,就得到了想要的物理地址了
paddr = page_addr | page_offset;
printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
return paddr;
}
static int __init v2p_init(void) //内核模块的注册函数
{
unsigned long vaddr = 0 ;
printk("vaddr to paddr module is running..\n");
get_pgtable_macro();
printk("\n");
vaddr = __get_free_page(GFP_KERNEL); //在内核的ZONE_NORMAL中申请了一块页面,GFP_KERNEL标志指示优先从内核的ZONE_NORMAL中申请页框
if (vaddr == 0) {
printk("__get_free_page failed..\n");
return 0;
}
sprintf((char *)vaddr, "hello world from kernel"); //在地址中写入hello
printk("get_page_vaddr=0x%lx\n", vaddr);
vaddr2paddr(vaddr);
return 0;
}
static void __exit v2p_exit(void) //内核模块的卸载函数
{
printk("vaddr to paddr module is leaving..\n");
free_page(vaddr); //将申请的线性地址空间释放掉
}
module_init(v2p_init);
module_exit(v2p_exit);
MODULE_LICENSE("GPL");
Makefile 如下
PROJ=Test05Virt2Physical
KERN_DIR=/lib/modules/$(shell uname -r)/build
obj-m+=$(PROJ).o
all:
make ARCH=x86_64 -C $(KERN_DIR) M=$(shell pwd) modules
clean:
make -C $(KERN_DIR) M=$(shell pwd) clean
rm -rf modules.order
添加内核模块输出日志如下, 这里展示的就是一个 虚拟地址 转换为 物理地址 的过程
待调试虚拟地址为 0xffff9346f8bc6000
mm->pgd 中存储的是 pgd 的首部, 加上 pgd 的索引 294, 计算得到 pgd 的地址, 0xffff9346c7dc8930, pgd 的值为 0x29275067
pgd 的值为 0x29275067, 存储数据的页地址为 0xffff9346e9275000, 加上 pud 的索引 283, 计算得到 pud 的地址, 0xffff9346e9275000 + 283 * 8 = 0xffff9346e92758d8, pud 的值为 0x29276067
pud 的值为 0x29276067, 存储数据的页地址为 0xffff9346e9276000, 加上 pmd 的索引 453, 计算得到 pmd 的地址, 0xffff9346e9276000 + 453 * 8 = 0xffff9346e9276e28, pmd 的值为 0x29276067
pmd 的值为 0x29276067, 存储数据的页地址为 0xffff9346f6f5a000, 加上 pte 的索引为 454, 计算得到 pte 的地址, 0xffff9346f6f5a000 + 454 * 8 = 0xffff9346f6f5ae30, pte 的值为 0x8000000038bc6063
pte 指向的即为对应的物理页信息, mask 掉后面 12bit, 加上偏移即为物理地址 8000000038bc6000
[ 3319.130839] vaddr to paddr module is running..
[ 3319.130842] cr0 = 0x80050033, cr3 = 0x7dc8000
[ 3319.130843] PGDIR_SHIFT = 39
[ 3319.130844] PUD_SHIFT = 30
[ 3319.130845] PMD_SHIFT = 21
[ 3319.130846] PAGE_SHIFT = 12
[ 3319.130847] PTRS_PER_PGD = 512
[ 3319.130848] PTRS_PER_PUD = 512
[ 3319.130848] PTRS_PER_PMD = 512
[ 3319.130849] PTRS_PER_PTE = 512
[ 3319.130850] PAGE_MASK = 0xfffffffffffff000
[ 3319.130853] get_page_vaddr=0xffff9346f8bc6000
[ 3319.130854] pgd_addr = 0xffff9346c7dc8930, *pgd = 0x29275067, pgd_page_addr = 0xffff9346e9275000, pgd_val = 0x29275067, pgd_index = 294
[ 3319.130856] pud_addr = 0xffff9346e92758d8, *pud = 0x29276067, pud_page_addr = 0xffff9346e9276000, pud_val = 0x29276067, pud_index = 283
[ 3319.130857] pmd_addr = 0xffff9346e9276e28, *pmd = 0x36f5a063, pmd_page_addr = 0xffff9346f6f5a000, pmd_val = 0x36f5a063, pmd_index = 453
[ 3319.130912] pte_addr = 0xffff9346f6f5ae30, *pte = 0x8000000038bc6063, pte_val = 0x8000000038bc6063, ptd_index = 454
[ 3319.130914] page_addr = 8000000038bc6000, page_offset = 0
[ 3319.130916] vaddr = ffff9346f8bc6000, paddr = 8000000038bc6000
[ 3326.641119] vaddr to paddr module is leaving..
基于 linux 的调试
处理缺页中断的时候, 根据 虚拟地址计算 pgd_index, 获取目标 pgd 的地址信息
根据虚拟地址 计算 pud_index, 如果给定的 pud 不存在, 则新建, 获取 pud 的地址信息
根据虚拟地址 计算 pmd_index, 如果给定的 pmd 不存在, 则新建, 获取 pmd 的地址信息
然后 接下来是 具体的分配物理页, 在 pmd 中记录 pte 的地址信息
pte_alloc_one 是我们这里能够看到的分配物理页的操作, 基于 alloc_pages 分配物理页
然后 pmd_populate 为记录当前 pmd 条目指向的 物理页的地址信息, 对应于上面 在 pmd 中存储 pte 的信息
pgd, pud, pmd, pte 的结构
此处结构为 x86_64 下的结构
pgd/pud/pmd/pte 每一个结构为 8字节 的条目
typedef struct { pgdval_t pgd; } pgd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct page *pgtable_t;
/*
* These are used to make use of C type-checking..
*/
typedef unsigned long pteval_t;
typedef unsigned long pmdval_t;
typedef unsigned long pudval_t;
typedef unsigned long pgdval_t;
typedef unsigned long pgprotval_t;
完
参考
Linux内核学习3——虚拟地址转换成物理地址z