(实验)把虚地址转化为物理地址
在经过教材中关于分段、分页、虚拟地址到线性地址及线性地址到物理地址转换的相关理论知识之后,需要对从虚拟地址到物理地址有更直观且真切的感受,故进行此次实验;
本次实验主要是对云课堂中2.4视频中操作的学习模仿,是在虚拟机上实现的,搭载的linux 5.15.0-83内核,本地主机是x86架构,支持四级分页;
以下是实验及原理分析的具体细节;
一、代码的运行&遇到的问题:
1.1、代码及输出结果:
paging具体代码如下:
#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();//读取cr0中数据的函数
cr3 = read_cr3_pa();//读取cr3中数据的函数
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_val = 0x%lx, pgd_index = %lu\n", 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(p4d, vaddr);
printk("pud_val = 0x%lx, pud_index = %lu\n", 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_val = 0x%lx, pmd_index = %lu\n", 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_val = 0x%lx, ptd_index = %lu\n", 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);
ssleep(100);//此处睡眠,会令进程阻塞;
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文件:
obj-m:=paging.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
上载及卸载内核模块后的输出截图:
1.2、根据输出结果的分析:
在输出日志文件中可以查看到:
- PGDIR_SHIFT和P4D_SHIFT都是39,意味着线性地址中没给P4D字段分配地址空间;
- 在页目录项中,P4D的页目录项为1,说明虽然Linux采用了五级页表模型,但实际上只使用了四个页表;
- PAGE_MASK(页面掩码):
0xffff ffff ffff f000
低12位均为0,其余位为1。该掩码目的是帮助操作系统内核在处理内存页面时进行掩码操作,以提取或对齐页面的偏移部分。 - get_page_vadrr给出了线性地址,paddr给出了具体的物理地址,通过线性地址找物理地址,一步一步验证就可得到我们放在物理地址中的“hello world from kernel!”;
1.3、所遇问题:
由于本处所用代码是在注释了视频中代码的基础上的搬运,故在运行期间出现了一些问题:
- 在插入内核模块paging.ko后,相关进程被阻塞,模块长时间运行。查看日志后看到如下问题:
- 在尝试了使用kill命令
kill -9 3310
后仍然不可强制终止该进程; - 再次阅读源码后发现是程序启用了
ssleep命令
,ssleep(600)
使进程每次都要睡眠10分钟才可结束,具体关于ssleep函数的内核源码分析可见本片文章结尾处; - 为了提高效率,将10min改为1min来缩短时间;
二、利用调试工具进行调试:
2.1、dram+fileview工具进行调试:
2.1.1、工具介绍及搭载:
-
dram内核模块:通过mmap将物理内存中的数据映射到设备文件==中,我们通
过对于这个设备文件进行访问,就可以达到访问物理内存的功能了;
-
fileview:按照想要的格式阅读这种二进制文件;
两个具体的详细代码见如下链接:
经过将dram.c内核模块上载、编译fileview.cpp文件后,可在dev目录下创建一个dram 其设备号为85,最后便可以用fileview工具访问刚刚创建的dram文件;相关终端指令可参考下面指令:
sudo insmod paging #上载内核模块paging;
g++ -o fileview fileview.cpp #编译fileview.cpp;
sudo mknod /dev/dram c 85 0 #创建dram文件;
./fileview /dev/dram #使用fileview工具;
运行结果如下:
2.1.2、调试过程(从虚拟地址找到物理地址)
在上一章中我们打印出了分页要用到的地址、寄存器cr3中存储的部分数据以及通过get_page_vadrr得到了线性地址。已知:
- cr3中页目录基地址:0x16448a000;
- 线性地址中第12位到第48位:0x99491eab6;
为了方便阅读,将具体信息汇总如下:
页目录 | 二进制 | 十进制(*8B) | 十六进制(*8B) | 基地址 | 物理地址 | 存储数据 |
---|---|---|---|---|---|---|
CR3 | / | / | 16448a000 | / | / | / |
PGD | 1001 1001 0 | 306*8B=2448 | 990 | 16448a | 16448a990 | 80e02067 |
P4D | 1001 1001 0 | 306*8B=2448 | 990 | / | / | / |
PUD | 1001 0010 0 | 292*8B=2336 | 920 | 80e02 | 80e02920 | 14ab51063 |
PMD | 0111 1010 1 | 245*8B=1960 | 7A8 | 14ab51 | 14ab517A8 | 160608063 |
PTE | 0101 1011 0 | 182*8B=1456 | 5B0 | 160608 | 1606085B0 | 15eab6163 |
找到 | / | / | / | / | 15eab6000 | hello world… |
计算过程:
-
cr3中存储页全局目录表基地址 + 线性地址中39-47位偏移量*8B:
- 0x16448a000 + 0x990 = 0x16448a990读取数据 —>0x80e02067(pgd_val的值,下一级页表物理地址)
-
P4D未启用,故不讨论;
-
PGD中存储PUD的基地址 + 线性地址中30-38位偏移量*8B:
- 0x80e02000 + 0x920 = 0x80e02920 读取数据 —>0x14ab51063(pud_val的值,下一级页表物理地址)
-
PUD中存储PMD的基地址 + 线性地址中21-29位偏移量*8B:
- 0x14ab51000 + 0x7A8 = 0x14ab517A8 读取数据—>0x160608063(pmd_val的值,下一级页表物理地址)
-
PUD中存储PMD的基地址 + 线性地址中21-29位偏移量*8B:
- 0x160608000 + 0x5B0 = 0x1606085B0 读取数据—>0x15eab6163(pte_val的值,页面物理地址)
-
页内偏移量page_offset为0,故最终物理地址为:0x15eab6000
- 0x15eab6000 + 0x0 = 0x15eab6000 读取数据—> hello world from kernel;
使用dram,fileview 调试过程如下截图:
三、结合原理分析地址转化过程:
3.1、虚拟地址到线性地址(linux中的分段):
linux为了实现可移植,故不启用段机制,只用分页机制来实现虚拟地址到物理地址的映射;
linux如何跳过段机制直接到分页机制的?
- 段基地址设为0;
- 段界限设为4GB;
- linux中设四个段描述符:
- 内核代码段;
- 内核数据端;
- 用户代码段;
- 用户数据段;
3.2、从线性地址到物理地址(linux中的分页机制):
3.2.1、分页机制:
通过分页机制来实现从线性地址到物理地址;
书本中介绍了两级页表的寻址过程,具体寻址过程可参考下面的笔记(32位):
3.2.2、linux中分页机制分析:
由于是64位处理器,所以显示的地址是64位,但由于64位处理器硬件的限制,地址线只有48条,所以线性地址和物理地址实际使用的也只有48位,在64位Linux中使用了4级页表结构,它的线性地址划分如下图所示,在这种情况下页面的大小都为4Kb,每一个页表项大小为8bit,整个页表可以映射的空间是256TB。
而新的Intel芯片的MMU硬件规定可以进行5级的页表管理,所以在4.15的内核中,Linux在页全局目录和页上级目录之间又增加了一个新的页目录,叫做P4D页目录(在PGD和PUD之间)。CR3寄存器用来保存当前进程的页全局目录的地址,寻页的开始就是从页全局目录开始的。
根据实验中的数据及分页原理绘制如下图:
其中具体计算过程如下:
-
cr3中存储页全局目录表基地址 + 线性地址中39-47位偏移量*8B:
- 0x16448a000 + 0x990 = 0x16448a990读取数据 —>0x80e02067(pgd_val的值,下一级页表物理地址)
-
P4D未启用,故不讨论;
-
PGD中存储PUD的基地址 + 线性地址中30-38位偏移量*8B:
- 0x80e02000 + 0x920 = 0x80e02920 读取数据 —>0x14ab51063(pud_val的值,下一级页表物理地址)
-
PUD中存储PMD的基地址 + 线性地址中21-29位偏移量*8B:
- 0x14ab51000 + 0x7A8 = 0x14ab517A8 读取数据—>0x160608063(pmd_val的值,下一级页表物理地址)
-
PUD中存储PMD的基地址 + 线性地址中21-29位偏移量*8B:
- 0x160608000 + 0x5B0 = 0x1606085B0 读取数据—>0x15eab6163(pte_val的值,页面物理地址)
-
页内偏移量page_offset为0,故最终物理地址为:0x15eab6000
- 0x15eab6000 + 0x0 = 0x15eab6000 读取数据—> hello world from kernel;
3.2.3、结合源码进行分析:
在前面的章节中已经贴了本次实验的代码及注释,以此为基础结合linux内核中的源码进行相关分析,其中涉及到了下面几个函数;
宏或函数 | 说明 |
---|---|
pgd_offset(mm, addr) | 根据入参内存描述符mm和虚拟地址address,找到address在页全局目录中相应表项的物理地址。 |
p4d_offset(pgd, addr) | 根据入参pgd和虚拟地址address,找到address在页四级目录中相应表项的物理地址。 |
pud_offset(p4d,addr) | 根据入参p4d和虚拟地址address,找到address在页上级目录中相应表项的物理地址。 |
pmd_offset(pud, address) | 根据入参pud和虚拟地址address,找到address在页中间目录中相应表项的物理地址。 |
pte_index(address) | 根据入参虚拟地址address,找到address在页表中索引。 |
set_pgd(pgdp, pgd) | 向PGD写入指定的值 |
set_p4d(p4dp, p4d) | 向P4D写入指定的值 |
这里的pgd_offset(mm, addr)
、p4d_offset(pgd, addr)
、pud_offset(p4d,addr)
、pmd_offset(pud, address)
、pte_offset_kernel(pmd, vaddr);
五个函数均是通过给定基地址+偏移量,来查找下一级页目录的地址,当然这里入参第一位及返回的均是一个结构体。
第一个参数是当前进程的mm_struct结构,mm_struct结构是用来描述进程的虚拟地址空间的,在
mm_struct中有个字段PGD就是用来保存该进程的页全局目录的物理地址的。这行代码找到了PGD表项
的物理地址pgd,该物理地址下存放的是下级页表的物理地址
p4d = p4d_offset(pgd, vaddr);
这行代码找到了P4D表项的物理地址p4d,由于页四级目录没有启用,所以目录表项为1,即
p4d=pgd。以此类推······,最后:
pte = pte_offset_kernel(pmd, vaddr);
到此获得PTE表项的物理地址pte,该物理地址下存放的是页面物理地址,对应于两级页表的32位线
性地址到物理地址转换的第三步。
page_addr = pte_val(*pte) & PAGE_MASK; /*取出其高52位*/
/*取出页偏移地址,页偏移量也就是线性地址中的低12位*/
page_offset = vaddr & ~PAGE_MASK;
/*将两个地址拼接起来,就得到了想要的物理地址了*/
paddr = page_addr | page_offset;
最后一步都是取其高位(64位取高52位,32位取高20位)与线性地址的低12位即偏移量拼接起
来,结果就是物理地址。
3.3、分页之个人理解:
对于分页过程,我个人认为就是一层一层的寻址,由于连续的物理地址空间有限,所以不断地经过多级分页,最终只要求有一片连续空间(页全局目录所占空间),其余的页上级目录、页中间目录等等都不需要连续的物理地址空间,从而减轻内存空间的压力。
四、提出问题:
4.1、在前面1.3中遇到了被阻塞问题,请详细分析为何被阻塞?
前面1.3中分析知道进程被长期阻塞是因为ssleep(100)函数,而ssleep函数是如何实现睡眠的呢?在linux内核源码中被定义在 /include/linux/delay.h ,而其中涉及到的msleep()函数则在/kernel/time/timer.c文件中。
static inline void ssleep(unsigned int seconds)
{
msleep(seconds * 1000);
}//ssleep通过调用msleep函数让其睡眠seconds秒
/**
* msleep - sleep safely even with waitqueue interruptions
* @msecs: Time in milliseconds to sleep for
*/
void msleep(unsigned int msecs)//传入要睡眠的具体时间;
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;//调用msecs_to_jiffies函数
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);//休眠指定时间;
}
- ssleep函数调用了内核中的函数msleep函数,并传入要休眠的具体时间;
- msleep函数先调用函数msecs_to_jiffies,将具体的毫秒时间转化为内核的滴答数;
- msleep函数再调用schedule_timeout_uninterruptible(),使当前执行的内核线程休眠指定时间;
4.2、请详细说明pgd_offset()函数是如何实现的?
具体实现过程可参考下面的笔记:
文件/include/linux/pgtable.h中pgd_offset()函数源码:
static inline pgd_t *pgd_offset_pgd(pgd_t *pgd, unsigned long address)
{
return (pgd + pgd_index(address));
};
/*
* a shortcut to get a pgd_t in a given mm
*/
#ifndef pgd_offset
#define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))
#endif
其中pgd_offset_pgd()函数中调用了pgd_index()函数,在文件/include/linux/pgtable.h下,其源码如下所示:
#ifndef pgd_index
/* Must be a compile-time constant, so implement it as a macro */
#define pgd_index(a) (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#endif
ex(address));
};
/*
* a shortcut to get a pgd_t in a given mm
*/
#ifndef pgd_offset
#define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))
#endif
其中pgd_offset_pgd()函数中调用了pgd_index()函数,在文件/include/linux/pgtable.h下,其源码如下所示:
#ifndef pgd_index
/* Must be a compile-time constant, so implement it as a macro */
#define pgd_index(a) (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#endif
与pgd_offset()函数相似,p4d_offset(pgd, addr)
、pud_offset(p4d,addr)
、pmd_offset(pud, address)
逻辑道理一致;