在Linux中,内核的页面映射机制分为三层,页面目录和页面表中间设有一个“中间目录”。中间目录是为了对64位CPU兼容而设计的。在内核代码中,页面目录称为PGD,中间目录称为PMD,页面表成为PT, PT中的表项称为PTE, 是“page table entry的缩写”。
一个进程的线性地址从高位到低位划分为4个段,各占若干位,分别作用为目录PGD中的下标,中间目录PMD的下标、页面表的下标以及物理页面内的位移。因此,给定一个线性地址,利用其前面三个段,就可以得到这个线性地址对应的pte。本文将分别阐述如何在X86以及龙芯2f下通过线性地址来得到对应的pte。
我们都知道,每一个进程都有一个task_struct结构,这个结构也就是我们通常所说的PCB。linux内核提供一个指针current,该指针指向当前进程的task_struct结构。通过current指针我们能得到当前进程的mm_struct结构。mm_struct结构是进程整个用户空间的一个抽象。通过进程的mm_struct我们就可以得到进程的pgd,然后按照线性地址的组成,我们就可以一步一步的得到线性地址对应的pte。
我的实验是基于这么一个框架完成的:写一个模块,模块注册一个设备,我们可以通过write系统调用向这个设备中写一个线性地址,模块会通过printk打印出对应的pte。
让我们现来分析下从32位的x86下如何得到线性地址的pte。在32为的x86下,线性地址为32bit,PGD位段大小为10,也就是说线性地址前10位表示目录项在页面目录中的下标,通过这个下标就能得到对应的目录项。因为i386结构物理上是二层映射,所以linux把PMD的位段大小设为0,也就是把PMD给mask掉了。目录项逻辑上指向一个大小为1的中间目录PMD,但是物理上直接指向相应的页面表(PT),i386的内存管理单元并不知道PMD的存在。PT的位段大小也为10,以PT位段为下标就能得到相应的表项PTE。
在实际的操作中,内核提供了一些函数来简化这些取位的操作,函数将在代码中做简单的介绍。这里主要介绍取pte的方法和思路,对模块实现的细节也不做解释。
X86下实现该模块的代码如下:
#include
#include
#include
#include
#include
#include
//#include
#include
#include
MODULE_LICENSE("Dual BSD/GPL");
/*这个函数的目的是把一个字符串类型的线性地址转化成整型,可以忽略这个函数的实现过程*/
int convert_vaddr(char *buff, int count){
//该函数的输入是一个字符串和字符串长度,如"8048000, 8",输出是一个int型整数,如0x8048000
//具体形式在下面给出
}
/*模块的主要函数,该函数输入一个线性地址,然后将该地址对应的pte打印出来*/
int event_write(struct file *filp, char __user *buff, size_t count,
loff_t *f_ops){
struct mm_struct *mm;
pgd_t *pgd;
pmd_t *pmd;
pud_t *pud;
pte_t *pte;
char buff_k[count];
int vaddr;
unsigned long pa;
if(copy_from_user(buff_k,buff,count)) //将线性地址从用户空间拷贝到内核空间
return -EFAULT;
if((vaddr = convert_vaddr(buff_k,count))==-EINVAL) //将字符串类型的线性地址转换成int型
return -EINVAL;
mm = current->mm; //获得当前进程的mm_struct
pgd = pgd_offset(mm, vaddr); //pgd_offset函数通过mm_struct和线性地址得到该地址对应的页面目录项
pud = pud_offset(pgd,vaddr); //pud是page uper directory,是pgd和pmd之间的一层映射,在i386的两级映射中不起作用
pmd = pmd_offset(pud,vaddr); //这里使用pud_offset和pmd_offset仅讲pgd的值类型转换后传承下来。
pte = pte_offset_kernel(pmd, vaddr); //pte_offset_kernel 根据线性地址和pmd,找到该线性地址对应的页表项。
pa = pte_val(*pte) ; //得到pte的内容
printk("current process's pte is:%x\n",pa);
printk("