一文搞懂Linux内存映射实现(二)

【好文推荐】

需要多久才能看完linux内核源码?

路由选择协议——RIP协议

轻松学会linux下查看内存频率,内核函数,cpu频率

概述Linux内核驱动之GPIO子系统API接口

一篇长文叙述Linux内核虚拟地址空间的基本概括

在分析mapdrv_mmap这个函数之前,还要补充mmap系统调用的执行过程。一个用户进程调用了mmap系统调用之后,它需要依次执行下图那么多函数

首先用户进程在调用mmap系统调用之后,系统会为其在当前进程的虚拟地址空间中寻址一段连续的空闲地址,这是通过遍历vm_area_struct链表来实现。当找到了合适的这样的一段区间之后,会为其建立一个vm_area_struct结构,完成这些之后,该进程就有了一个专门用于mmap映射的虚拟内存区了。但是这样还不够,因为在进程页表中,这个区域的线性地址都没有对应的物理页框,接着系统会调用内核空间的系统调用函数mmap,也就是需要我们在file operations(f_op)结构体中定义的这个mmap,它将要完成对vm_area_struct结构中的虚拟地址建立其相应的页表项

而建立页表项的方法有两种,一种是remap_pfn_range方法,一种叫fault的方法,两者的区别是remap_pfn_range方法是在内核函数mmap,即file_operations中要完成的mmap函数,它在被调用时一次性的为vm_area_struct结构中的这些线性地址建立页表项,所以这也就要求了这些页表项所要映射的物理地址是连续的。

后者fault函数则不同,它是在进程访问到这个映射空间中的虚拟地址时,发现虚拟地址的页表项为空,引起了缺页时才被调用,所以它更适合于这里,我们对vmalloc分配的这些不连续的物理地址来进行映射,所以这里我们将使用fault方法。

回到驱动程序中,如下的一些判断,都是对异常情况的判断。

首先MAPLEN是之前定义的宏,它用来表示在内核中申请的物理页框的大小,如果进程中用来映射的线性地址区大于我们申请的物理地址if(size > MAPLEN),那么就可能访问到一些不该访问的地方,所以我们需要防止这种情况的发生。

接着这个判断if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)),如果我们的vm区是可写入的,它的标志位是可写入的,vm_flags用来保存进程对该虚存空间的访问权限。如果我们当前的vma是可写入的话VM_WRITE,那么他也就必须有另一个标志位VM_SHARED,也就是可共享的,如果没有可共享的标志位的话,那么写入操作就应当是非法的,我们也要把这种情况判断出来。

vma->vm_flags |= VM_LOCKONFAULT;

给vm_flags标志位增加了一个标志位,叫做VM_LOCKONFAULT,它是用来锁住该区域所映射的这些物理页框的。让它不用被操作系统交换出去

整个mmap函数最重要的,其实是这里,“vm->vm_ops = &map_vm_ops”,它将我们在驱动模块中定义的vm_operations_struct结构的地址赋值给了当前vma的vm_ops指针,也就是说它用我们在驱动模块中定义的vm_operations结构体替换掉了当前进程vma中的这样一个结构体,为什么要这么做呢?想想刚才提到的fault方法,fault方法在建立页表的时候,他是在进程访问到线性地址产生缺页时才建立页表的,所以我们在mmap函数被调用的时候,可能我们的用户进程还没有进行访问,所以呢,我们建立页表还不用现在就立即建立,所以将这个任务交给它的下一环节,即vm_operations结构体。

static int mapdrv_mmap(struct file *file, struct vm_area_struct *vma)  
{  
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;  
    unsigned long size = vma->vm_end - vma->vm_start;  
   
    if (size > MAPLEN) {  
        printk("size too big\n");  
        return -ENXIO;  
    }  
    /*  only support shared mappings. */  
    if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)) {  
        printk("writeable mappings must be shared, rejecting\n");  
        return -EINVAL;  
    }  
    /* do not want to have this area swapped out, lock it */  
    vma->vm_flags |= VM_LOCKONFAULT;  
    if (offset == 0) {  
        vma->vm_ops = &map_vm_ops;   
    } else {  
        printk("offset out of range\n");  
        return -ENXIO;  
    }  
    return 0;  
}

最后介绍的是vmoperation_struct结构体和fault 函数,以下是驱动模块中定义的vm_operation_struct结构体

static struct vm_operations_struct map_vm_ops = {
	.open = map_vopen,
	.close = map_vclose,
	.fault = map_fault,
};

在内核源码中的完整定义,在include/linux/mm.h中

如下,发现vm_operations_struct结构体与file_operations结构体非常类似,其中也定义很多函数指针,可以有选择的实现它们。我们这里主要实现fault函数。vm_fault_t (*fault)(struct vm_fault *vmf);

关于fault函数的功能前面已经说过,它是用来为进程中用来映射的线性地址建立相应的页表项的,这里看出,传入的参数只是一个指针,在不同内核版本中,fault函数的表现形式有所不同。在更早的内核版本中可能没有这个函数,但其实与nopage函数功能类似。

fault函数体,传入指向vm_fault结构体的指针,

struct vm_operations_struct {
	void (*open)(struct vm_area_struct * area);
	void (*close)(struct vm_area_struct * area);
	int (*split)(struct vm_area_struct * area, unsigned long addr);
	int (*mremap)(struct vm_area_struct * area);
	vm_fault_t (*fault)(struct vm_fault *vmf);
	vm_fault_t (*huge_fault)(struct vm_fault *vmf,
			enum page_entry_size pe_size);
	void (*map_pages)(struct vm_fault *vmf,
			pgoff_t start_pgoff, pgoff_t end_pgoff);
	unsigned long (*pagesize)(struct vm_area_struct * area);
    /* notification that a previously read-only page is about to become
     * writable, if an error is returned it will cause a SIGBUS */
    vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);

    /* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
    vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);

    /* called by access_process_vm when get_user_pages() fails, typically
     * for use by special VMAs that can switch between memory and hardware
     */
    int (*access)(struct vm_area_struct *vma, unsigned long addr,
              void *buf, int len, int write);

    /* Called by the /proc/PID/maps code to ask the vma whether it
     * has a special name.  Returning non-NULL will also cause this
     * vma to be dumped unconditionally. */
    const char *(*name)(struct vm_area_struct *vma);
#ifdef CONFIG_NUMA
	/*
	 * set_policy() op must add a reference to any non-NULL @new mempolicy
	 * to hold the policy upon return.  Caller should pass NULL @new to
	 * remove a policy and fall back to surrounding context--i.e. do not
	 * install a MPOL_DEFAULT policy, nor the task or system default
	 * mempolicy.
	 */
	int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
/*
	 * get_policy() op must add reference [mpol_get()] to any policy at
     * (vma,addr) marked as MPOL_SHARED.  The shared policy infrastructure
     * in mm/mempolicy.c will do this automatically.
     * get_policy() must NOT add a ref if the policy at (vma,addr) is not
     * marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
     * If no [shared/vma] mempolicy exists at the addr, get_policy() op
     * must return NULL--i.e., do not "fallback" to task or system default
     * policy.
     */
	struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
				unsigned long addr);
#endif
	/*
	 * Called by vm_normal_page() for special PTEs to find the
	 * page for @addr.  This is useful if the default behavior
	 * (using pte_page()) would not find the correct page.
	 */
	struct page *(*find_special_page)(struct vm_area_struct *vma,
					  unsigned long addr);
};

下面是内核中关于vm_fault结构体的定义,它是用来存放与缺页相关的参数。

这里我们可以看到首先是映射区vma指针struct vm_area_struct *vma

unsigned long address 就是产生缺页的线性地址

pmd/pud 就是线性地址所对应的页目录项

pte就是它所对应的页表项

这个结构体中,最重要的是page字段,struct page *page; 我们在fault函数中要为这个用来映射的线性地址建立相应的页表项,那么我们首先要找到它所对应的物理地址,找到这个物理地址后,我们取到它的页描述符,将它的页描述符填写到vm_fault结构体的page字段中,剩下的建立页表项的工作就可以交由操作系统自动完成了。

struct vm_fault {
	struct vm_area_struct *vma;	/* Target VMA */
	unsigned int flags;		/* FAULT_FLAG_xxx flags */
	gfp_t gfp_mask;			/* gfp mask to be used for allocations */
	pgoff_t pgoff;			/* Logical page offset based on vma */
	unsigned long address;		/* Faulting virtual address */
	pmd_t *pmd;			/* Pointer to pmd entry matching
					 * the 'address' */
	pud_t *pud;			/* Pointer to pud entry matching
					 * the 'address'
					 */
	pte_t orig_pte;			/* Value of PTE at the time of fault */
    struct page *cow_page;		/* Page handler may use for COW fault */
    struct mem_cgroup *memcg;	/* Cgroup cow_page belongs to */
    struct page *page;		/* ->fault handlers should return a
                     * page here, unless VM_FAULT_NOPAGE
                     * is set (which is also implied by
                     * VM_FAULT_ERROR).
                     */
    /* These three entries are valid only while holding ptl lock */
    pte_t *pte;			/* Pointer to pte entry matching
                     * the 'address'. NULL if the page
                     * table hasn't been allocated.
                     */
    spinlock_t *ptl;		/* Page table lock.
                     * Protects pte page table if 'pte'
                     * is not NULL, otherwise pmd.
                     */
    pgtable_t prealloc_pte;		/* Pre-allocated pte page table.
                     * vm_ops->map_pages() calls
                     * alloc_set_pte() from atomic context.
                     * do_fault_around() pre-allocates
                     * page table to avoid allocation from
                     * atomic context.
                     */
};

了解了这些之后,再回到驱动代码

可以看到fault函数在驱动中是这样一个名字“.fault = map_fault,”

static struct vm_operations_struct map_vm_ops = {
	.open = map_vopen,
	.close = map_vclose,
	.fault = map_fault,
};

这个驱动程序的目的是将内核空间的线性地址所对应的物理地址映射到用户空间的某一个线性地址中。所以我们首先需要找到这些物理页框。那么我们怎么找呢?用到的方法就是这两个函数,vmalloc_to_pfn和vmalloc_to_page,顾名思义,这两个函数可以将内核vmalloc区线性地址所指向的那个物理页框它找到,我们想要的是页描述符,那么我们就用vmalloc_to_page函数,我们想要找到页帧号,那么我们就有vmalloc_to_pfn函数。

我们从头看这个fault函数,首先offset(offset = vmf->address-vmf->vma->vm_start;),它是我们产生缺页的线性地址在这个vma映射区中的偏移量,我们用当前的缺页地址“vmf->address"减去vma的起始地址"vmf->vma->vm_start”得到它的offse

接下啦的virt_strat,它是内核中的线性地址,我们用的是vmalloc_area,也就是我们在模块装载函数中申请到的那个线性区的起始地址让它加上每一页的偏移“(vmf->pgoff << PAGE_SHIFT)”,就得到了内核中每一页的线性地址

pfn_start就是这些对应的内核中的线性地址所对应的那些物理页框的页帧号,这里我们使用的是vmalloc_to_pfn函数来找到它们

下面的if语句,就是对异常情况的判断, 比如当前进程的vma为空(“if((vmf->vmaNULL)||(vmalloc_areaNULL))”),申请到的内核中的内存区域为空。那么肯定是一种异常情况,我们将它判断出来。

接着,这个判断“if(offset >= MAPLEN)”是如果我们当前产生缺页的线性地址,它超过了我们在内核中用于映射的物理页框的大小,这样的异常我们需要将它判断出来。

这里的page_ptr(page_ptr=vmalloc_area + offset),它是内核中的线性地址,其实跟virt_start的值是一样的这里我们用vmalloc_area,即它的起始地址加上offset每一页的偏移量得到的

这里的page(page=vmalloc_to_page(page_ptr);),因为我们要取得是页描述符,所以我们就用vmalloc_to_page函数,将内核中的线性地址,找到它所对应的物理页框的页描述符,

接着需要将找到的每一页(get_page(page);),物理页框增加它的引用次数,用get_page(page)这样一个函数来实现

最后(vmf->page = page;)我们将找到的页描述符填写到vmf的page字段中,就完成了我们对一个产生缺页的线性地址的页表项的建立

“printk("%s: map 0x%lx (0x%016lx) to 0x%lx, sie:0x%lx, page:%ld \n",func,virt_start,pfn_start << PAGE_SJIFT, vmf->address,PAGE_SIZE,vmf->pgoff);” 取出内核中的线性地址所对应的物理地址,还有映射后的进程中的线性地址,统统打印出来

int map_fault(struct vm_fault *vmf)  
{  
	struct page *page;
	void *page_ptr;
        unsigned long offset, virt_start, pfn_start;	
        offset = vmf->address-vmf->vma->vm_start;
        virt_start = (unsigned long)vmalloc_area + (unsigned long)(vmf->pgoff << PAGE_SHIFT);
        pfn_start = (unsigned long)vmalloc_to_pfn((void *)virt_start);

	printk("\n");    
	/*printk("%-25s %d\n","7)PAGE_SHIFT",PAGE_SHIFT);*/
	page_ptr=NULL;
	if((vmf->vma==NULL)||(vmalloc_area==NULL)){
		printk("return VM_FAULT_SIGBUS!\n");
		return VM_FAULT_SIGBUS;
	}
	if(offset >=MAPLEN){
		printk("return VM_FAULT_SIGBUS!");
		return VM_FAULT_SIGBUS;
	}
	page_ptr=vmalloc_area + offset;
	page=vmalloc_to_page(page_ptr);
	get_page(page);	
	vmf->page=page; 
        printk("%s: map 0x%lx (0x%016lx) to 0x%lx , size: 0x%lx, page:%ld \n", __func__, virt_start, pfn_start << PAGE_SHIFT, vmf->address,PAGE_SIZE,vmf->pgoff);

	return 0;
}

到这里就是有关于驱动模块的主要内容了,在结束之前再看一下前面有关一些宏的定义

我们在内核空间中申请了10个 页面大小的内存空间,主设备号是240,设备名称是mapnopage

#define MAP_PAGE_COUNT 10
#define MAPLEN (PAGE_SIZE*MAP_PAGE_COUNT)
#define MAP_DEV_MAJOR 240
#define MAP_DEV_NAME "mapnopage"

在设备注册函数中,我们不光申请了10个页面的内存“vmalloc_area=vmalloc(MAPLEN)”,并且在这里“sprintf((char *)virt_addr,“test %d”,i++);}” ,在每一页的内存中都写入了一个test字符串,从第一页开始依次是test1,test2,test3一直到test10

static int __init mapdrv_init(void)
{
	int result;
	unsigned long virt_addr;
	int i = 1;
	result = register_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME,&mapdrvo_fops);
	if(result<0){
		return result;
	}
	vmalloc_area=vmalloc(MAPLEN);
	virt_addr = (unsigned long)vmalloc_area;
	for(virt_addr =(unsigned long)vmalloc_area;virt_addr < (unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE);
	{
		SetPageReserved(vmalloc_to_page(void *)virt_addr));
		sprintf((char *)virt_addr,"test %d",i++);}
	}
	/* printk("vmalloc_area at 0x%lx (phys 0x%lx)\n",(unsigned long)vmalloc_area,(unsigned long)vmalloc_to_pfn(void *)vmalloc_area << PAGE_SHIFT); */
	printk("vmalloc area apply complete!");
	return 0;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值