序文
へー、これは 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");
メイクファイルは以下の通り
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 を作成し、pud のアドレス情報を取得します。
仮想アドレスに基づいて pmd_index を計算し、指定された pmd が存在しない場合は新しい pmd を作成し、pmd のアドレス情報を取得します。
次に、物理ページを具体的に割り当て、pte のアドレス情報を pmd に記録します。
pte_alloc_one は、ここで確認できる物理ページを割り当てる操作であり、alloc_pages に基づいて物理ページを割り当てます。
次に、pmd_populate は、上記の pmd の pte に格納されている情報に対応する、現在の pmd エントリが指す物理ページのアドレス情報を記録します。
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;
以上