在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-&gt;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("$\n");
	return 0;
}
 
struct cdev event;
struct file_operations event_ops;
//init function
static int init_event(void){
 
	//register the device event
	if(register_chrdev_region(MKDEV(33,0),1,"readpte")){
		printk("register error!!\n");
		return -EFAULT;
	}
 
	//register operations
	event_ops.write = event_write;
 
	cdev_init(&amp;event, &amp;event_ops);
	cdev_add(&amp;event,MKDEV(33,0),1);
 
	return 0;
 
}
 
//clean function
static void exit_event(void){
 
	//unregister device
	cdev_del(&amp;event);
	unregister_chrdev_region(MKDEV(33,0),1);
 
}
 
module_init(init_event);
//module_init(init_event);
module_exit(exit_event);

为了方便大家使用代码,保证代码完整性,下面给出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("$\n");
 
    return 0;
 
}
anyShare分享到:
Tagged with:
 

2 Responses to “Linux下通过线性地址得到页表项pte(X86和龙芯2F下)”

  1. littlebeauty says:

    亲爱的,你真厉害*^__^*

Leave a Reply

Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).