在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 <linux/module.h> #include <linux/init.h> #include <linux/sched.h> #include <asm/uaccess.h> #include <asm/current.h> #include <asm/pgtable.h> //#include <asm/pgtable-32.h> #include <linux/fs.h> #include <linux/cdev.h> 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(" |
为了方便大家使用代码,保证代码完整性,下面给出convert_vaddr函数的代码:
int convert_vaddr(char *buff, int count){ int i; int ret=0; for (i = 0;i<count-1;i++){ //printk("loop i is:%d, buff[i] is %c\n",i, buff[i]); switch (buff[i]){ case '0': break; case '1': ret = ret+(1<<((count-2-i)*4)); break; case '2': ret = ret+(2<<((count-2-i)*4)); break; case '3': ret = ret+(3<<((count-2-i)*4)); break; case '4': ret = ret+(4<<((count-2-i)*4)); break; case '5': ret = ret+(5<<((count-2-i)*4)); break; case '6': ret = ret+(6<<((count-2-i)*4)); break; case '7': ret = ret+(7<<((count-2-i)*4)); break; case '8': ret = ret+(8<<((count-2-i)*4)); break; case '9': ret = ret+(9<<((count-2-i)*4)); break; case 'a': ret = ret+(0xa<<((count-2-i)*4)); break; case 'b': ret = ret+(0xb<<((count-2-i)*4)); break; case 'c': ret = ret+(0xc<<((count-2-i)*4)); break; case 'd': ret = ret+(0xd<<((count-2-i)*4)); break; case 'e': ret = ret+(0xe<<((count-2-i)*4)); break; case 'f': ret = ret+(0xf<<((count-2-i)*4)); break; default: printk("error vaddr!!\n"); return -EINVAL; } } return ret; } |
在龙芯2F下获得PTE的基本方法大致跟x86一样,但是也有一些区别,比如龙芯2F下PMD的位段不是0,因此PMD是有效的。
下面给出龙芯2F下获得PTE的模块代码,这次将主要通过对线性地址的位操作来实现。注意龙芯的页大小为16k,虚拟地址空间是64位,并且兼容32位,我们这里之考虑32位地址。隐藏一切无关的细节,我们需要注意的是,在32位的地址空间中,龙芯线性地址pgd位段大小为0,PMD位段大小为7,PTE位段大小为11。
为节约篇幅,将只给出主要功能函数event_write代码,将这个函数替换上面x86代码下的同名函数就能得到一个完整的代码。
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; pte_t *pte; char buff_k[count]; int vaddr; int pmd_entry, pte_entry; if(copy_from_user(buff_k,buff,count)) return -EFAULT; if((vaddr = convert_vaddr(buff_k,count))==-EINVAL) return -EINVAL; mm = current->mm; pgd = mm->pgd; //pgd此时指向页面目录的首地址,因为pgd的位段大小为0,所以页面目录只有一项,指向pmd pmd=(pmd_t*)(pgd->pgd); //pmd指向中间目录首地址 pmd_entry = (vaddr>>25)&(0x7f); //取线性地址的高7位,计算其在中间目录中的下标 pmd = pmd+pmd_entry; //得到中间目录项 pte = (pte_t *)(((pmd->;pmd)>>14)<<14); //中间目录项的前18位指向页面表,后面加上14个0后就是该页面表的地址 //这里pte就指向页面表的首地址 pte_entry = (((vaddr & 0x1ffc000)>>14) & 0x7ff); //取线性地址的中间11位,计算其在页面表中的下标 pte = pte+pte_entry; //得到页面表项,即PTE printk("current process's pte is:%x\n",(unsigned int)(pte->pte)); //输出页面表项的值 printk(" |
亲爱的,你真厉害*^__^*