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

【好文推荐】

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

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

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

下面介绍一下Linux内存映射的实现

一、基础概念

1、mmap文件映射

mmap是一种内存映射文件的方法,将一个文件映射到进程的地址空间,建立文件磁盘地址和进程虚拟地址的一种对应关系,如此进程通过读取相应的虚拟地址就可以直接读取相应文件中的内容。mmap是一种内存映射文件的方法,将一个文件映射到进程的地址空间,建立文件磁盘地址和进程虚拟地址的一种对应关系,如此进程通过读取相应的虚拟地址就可以直接读取相应文件中的内容。

这样映射的最大好处是进程可用直接访问内存,避免了频繁的使用read/write等文件系统的系统调用。需要注意的是mmap并不分配物理内存,它所做的最重要的工作就是为进程映射区的虚拟地址建立页表项

从图上可以看出进程的虚拟地址空间,是由多个虚拟内存区域构成的。如图所示的text数据段,初始数据段,bss数据段,堆,栈都是一个个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆和栈之间的空余部分。

2、进程的虚拟地址空间

Linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个虚拟内存区域功能和内部机制都不尽相同,因此一个进程会使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。即我们在上一页看到的text数据段,bss数据段等等。每个vm_area_struct都对应虚拟地址空间上一段连续的地址,它们之间使用链表或者树形结构链接,方便进程进行快速的查找/访问。

这里我们可用看到vm_area_struct结构中的一些字段,其中包括虚拟内存区的起始和结束的地址;vm_flags是该虚拟内存区的标志位。如果虚拟区域映射的是磁盘文件或者设备文件的话,那么vm_inode指向该文件的inode索引节点

这里有一个重要的vm_ops的字段,它是一个指向vm_operations_struct结构体的指针,在vm_operation_struct结构体中,定义了与该虚拟内存区操作相关的接口,其中包括了:open,close,fault等等这些操作。

每个虚拟内存区域都必须在vm_operations_struct结构中实现这些操作

一个进程的全部虚拟地址空间由mm_struct结构体来管理的,它里面包括了进程虚拟空间的一些管理的信息,包括进程pgd页表的地址等等,另外它还有一个指向,虚拟内存区链表的指针mmap。

最好,在进程描述符中有一个mm字段,指向mm_struct结构,这些共同组成了linux中进程虚拟地址空间的抽象描述

3、Linux中的字符设备驱动

最后是Linux中有关设备驱动的概念。

我们知道,所有的设备在linxu里面都是以设备文件的形式存在的,设备文件允许应用程序通过标准输入输出系统调用,与驱动程序进行交互,既然都是文件,当然也可以进行mmap映射,这是一种操作设备的方法。

有关设备和驱动的东西是一块很大的领域,此处以今天要使用的字符设备驱动为例,简单介绍它的基本内容。在用户程序看来,操作一个设备就是对设备文件的读写,而其具体的实现过程则是相应的驱动程序来完成的。

如上图示,方框中就是一个设备驱动的主要内容,它其中自然少不了对模块的加载和卸载函数,它们主要完成设备的初始化和删除的。它使用struct_cdev结构体来抽象描述一个字符设备,而每个cdev结构体就由一个dev_t类型的设备号来唯一指定,设备号分为主(major)设备号和次设备号,主设备号用来表明设备类型,次设备号用来表明其编号

【文章福利】小编在群文件上传了一些个人觉得比较好得学习书籍、视频资料,有需要的可以进群 【977878001】领取!!!额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车(免费报名):Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

这里可以看到由一个file_operations结构体,该结构体是linux文件系统中的一个非常重要的结构体,linux的VFS虚拟文件系统能够将不同类型的文件系统统一管理,并且为用户提供一个统一的接口,就是通过file_operations结构体实现的。

而在设备文件中,它主要用来存储驱动模块提供的对设备进行各种操作的函数,对于普通文件的read、write,驱动程序需要将其转化为对应的对设备的操作,就是通过该结构体(file_operations)完成的。它其中包括许多的钩子函数,包括read,release,mmap等等。read是进程在读设备文件时要做的,release则是在进程调用close时所要做的工作,它用来释放一些系统资源。最后是mmap,不同的文件有自己定义的mmap钩子,比如ext3文件系统对应了一个叫做generic_file_mmap的一个钩子函数

今天要做的主要工作,就是为一个虚拟字符设备编写其驱动模块,在其驱动中完成设备空间,即内核空间到用户空间的映射。

二、具体实现

进入源码,看内存映射具体的实现过程。 驱动程序源码map_driver.c

完整源码如下:

#include <linux/kernel.h>  	
#include <linux/module.h>  
#include <linux/fs.h>  
#include <linux/string.h>  
#include <linux/errno.h>  
#include <linux/mm.h>  
#include <linux/vmalloc.h>  
#include <linux/slab.h>  
#include <linux/sched.h>  
#include <asm/io.h>  
#include <linux/mman.h>  

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

extern struct mm_struct init_mm;  
void map_vopen(struct vm_area_struct *vma);
void map_vclose(struct vm_area_struct *vma);  
/*device mmap */  
static int mapdrv_mmap(struct file *file, struct vm_area_struct *vma);  
static int mapdrv_open(struct inode *inode, struct file *file); 
/* vm area nopage */  
int map_fault(struct vm_fault *vmf);  
  
static struct file_operations mapdrvo_fops = {  
    .owner = THIS_MODULE,  
    .mmap = mapdrv_mmap,  
    .open = mapdrv_open,
}; 

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

MODULE_LICENSE("GPL");  
  
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 complate!");
    return 0;
}  
  
static void __exit mapdrv_exit(void)  
{  
    unsigned long virt_addr;  
    /* unreserve all pages */  
    for(virt_addr = (unsigned long)vmalloc_area; virt_addr < (    unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE) 
    {  
        ClearPageReserved(vmalloc_to_page((void *)virt_addr));  
    }  
    /* and free the two areas */  
    if (vmalloc_area)
        vfree(vmalloc_area);  
    unregister_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME);
  
}  
  

  
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;  
}
static int mapdrv_open(struct inode *inoe, struct file *file)
{

    printk("process: %s (%d)\n", current->comm, current->pid);
    return 0;
}  
  
/* open handler for vm area */  
void map_vopen(struct vm_area_struct *vma)  
{  
    printk("mapping vma is opened..\n");
}  
  
/* close handler form vm area */  
void map_vclose(struct vm_area_struct *vma)  
{  
    printk("mapping vma is closed..\n");
}  
  
/* page fault handler */ 

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;
}

module_init(mapdrv_init);  
module_exit(mapdrv_exit);  

驱动程序大概有三部分组成,1-模块的装载卸载; 2-file_operations结构体和mmap函数;3-vm_operations_struct结构体和fault函数。

首先是模块的装载函数,它所要完成的工作是两个,一是设备的注册,二是在内核中为设备申请一块内存。

设备的注册由register_chrdev这个函数来实现,这里需要指定设备的主设备号MAP_DEV_MAJOR, 设备的名称MAP_DEV_NAME, 还有它所链接的file_operations 结构 &mapdrvo_fops);

这里如果主设备号为零,该设备将自己分配一个主设备号,返回给result。如果返回值为0,表示分配成功;返回值为负表示设备注册失败。

接下来是申请内存,此处用的是vmalloc函数,vmalloc函数的特点是申请的内存区域在内核的线性地址是连续的,但物理地址不连续。

这里我们看到这里还有为申请到的页框的PageReserved标志位置位,这样做是告诉系统,该物理页框已经被我使用。

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 complate!");
    return 0;
}  

下面是模块的卸载函数。在模块的卸载函数中,要做的正相反。

首先是清理PageReserved标志位,接着是通过vfree释放我们申请的vmalloc线性区的线性地址,最后是通过unregister_chrdev注销掉这个设备。

static void __exit mapdrv_exit(void)
{
	unsigned long virt_addr;
	/*unreserve all pages*/
	for(virt_addr = (unsigned long)vmalloc_area;virt_addr < (unsigned long)vmalloc_area + MAPLEN;virt_addr += PAGE_SIZE)
	{
		ClearPageReserved(vmalloc_to_page((void *)virt_addr));	
	}
	/* and free the two areas */
	if(vmalloc_area)
		vfree(vmalloc_area);
	unregister_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME);
	
}

接下来介绍的是file_operations结构体和mmap函数。如下为驱动程序中定义的file_operations结构体

驱动模块中只实现了三个功能,owner是用来指向该驱动module结构的指针

open函数在这里,我们用它来打印了调用该模块进程的pid

再就是mmap函数,具体分析过程在下面

static struct file_operations mapdrov_fops = {
	.owner = THIS_MODULE,
	.mmap = mapdrv_mmap,
	.open = mapdrv_open,
};

include/linux/fs.h

内核源码中有关file_operations结构体的完整定义如下,设备的读取,写入,保持等等这些操作,都是由存储在file_operations结构体中的这些函数指针来处理的,这些函数指针所指向的函数都需要我们在驱动模块中将其实现,在这里我们可以看到file_operations结构体中提到了许多的函数指针,包括read,write,mmap,open等。但是我们可以不全使用它们。对于那些指向未实现函数的只是可以简单的设置为空。操作系统将负责实现该功能。

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, bool spin);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值