内存映射

Linux 内存映射

在讲解内存映射之前,不得不去探讨Linux内存管理方面的知识。需要说明的是,我们并不需要深入的理解Linux虚拟内存才能去实现Linux的内存映射,所以对于Linux内存管理方面的知识也仅限于最基础的概念。

一、Linux的内存管理

Linux的内存管理子系统是采用请求调页式的虚拟存储器技术实现的,有关虚拟存储器方面的知识可以参考《深入理解计算机系统》第二版的第9章内容,在这里就不做说明。

  1Linux进程的虚拟空间及其划分

32位硬件平台上,Linux的逻辑地址为32位,因此,每个进程的虚拟地址空间为4GB,在4GB的空间中,操作系统占用了高端的1GB,而低端的3GB则留给用户程序使用。如下图所示:

内存映射_第1张图片

1) Linux内核虚拟存储器

Linux1GB的内核虚拟存储器空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域。

   一般情况下,物理内存映射区最大长度为896MB系统的物理内存被顺序映射到物理内存映射区中。当系统物理内存大于896MB时,超过系统物理内存的那部分内存称为高端内存(小于896MB的系统物理内存称为常规内存),内核在存取高端内存时必须将它们映射到高端内存映射区中。下图可以反映出Linux内核虚拟存储器与物理内存之间的映射关系。

内存映射_第2张图片

注意:物理内存中0~896MB区域通常由内核使用,当然内核不用时用户程序可以使用;896MB以上的区域通常由用户程序来使用。

2) Linux用户虚拟存储器

Linux用户虚拟存储器总是通过页表访问内存,决不会直接访问。如下图所示:

内存映射_第3张图片

  2、进程空间的描述

内核为系统中的每个进程维护一个单独的任务结构task_struct。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID、指向用户栈的指针、可执行目标文件的名字以及程序计数器)。

task_struct中的一个条目指向mm_struct,它描述进程使用的地址空间,我们感兴趣的两个字段是pgdmmap,其中pgd指向第一级页表(页全局目录)的基址,而mmap指向一个vm_area_struct(区域结构)的链表,每个vm_area_struct结构描述的是进程的一个用户区。如下图所示:

内存映射_第4张图片


二、Linux内存映射

当可执行文件准备运行时,可执行文件的内容仅仅映射到了对应进程虚拟地址空间中,而并没有调入物理内存。当程序开始运行并使用到这部分时,Linux才通过缺页中断把它们从磁盘上调入内存。这种将文件连接到进程虚拟地址空间的过程称为内存映射。

  1vm_area_struct结构

struct vm_area_struct {

struct mm_struct * vm_mm; /*所处的地址空间*/

unsigned long vm_start; /*开始的虚拟地址*/

unsigned long vm_end; /*结束的虚拟地址*/

pgprot_t vm_page_prot; /*访问权限*/

unsigned long vm_flags; /*标志,VM_IOVM_RESERVED*/

        //操作VMA的函数集

struct vm_operations_struct * vm_ops;

unsigned long vm_pgoff; /*偏移(页帧号)*/

struct file * vm_file; /*指向该区域(如果存在的话)相关联的file结构指针*/

void * vm_private_data; /*驱动程序用来保存自身信息的成员*/

};

vm_operations_struct结构的定义如下:

struct vm_operations_struct {

        //打开VMA的函数

void (*open)(struct vm_area_struct * area); 

        //关闭VMA的函数

void (*close)(struct vm_area_struct * area);

        //访问的页不在内存时调用

struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int *type);

unsigned long (*nopfn)(struct vm_area_struct * area, unsigned long address);

        //驱动程序不必实现populate方法

int (*populate)(struct vm_area_struct * area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);

};

  2、内存映射

一般情况下,用户空间是不可能也不应该直接访问设备的,但是,设备驱动程序中可实现mmap()函数,这个函数可使得用户空间能直接访问设备的物理地址。实际上,mmap()实现了这样的一个映射过程,它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。

3mmap设备操作

mmap方法是file_operations结构的一部分,mmap设备方法所需要做到就是建立虚拟地址到物理地址的页表。执行mmap系统调用时将调用该方法。使用mmap,内核在调用实际函数之前,就能完成大量的工作,因此该方法的原型与系统调用有着很大的不同。

系统调用有着以下的声明:

caddr_t  mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);

addr:指定文件应被映射到用户空间的起始地址,这样,选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。其类型caddr_t实际上就是void *

len:映射到调用用户空间的字节数,它从被映射文件开头offset个字节开始算起,offset参数一般设为0,表示从文件头开始映射。

prot:指定访问权限,PROT_READ(可读)PROT_WRITE(可写)PROT_EXEC(可执行)PROT_NONE(不可访问)

flagsMAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用

fd:为文件描述符,一般由open()返回,fd也可以指定为-1,此时需指定flags参数中的MAP_ANON,表明进行的是匿名映射。

但是文件操作声明如下:

int (*mmap) (struct file *, struct vm_area_struct *);

vma包含了用于访问设备的虚拟地址的信息,因此大量的工作由内核完成。为了执行mmap,驱动程序只需要为该地址返回建立合适的页表,并将vma->vm_ops替换为一系列的新操作就可以了。

注意:当用户调用mmap()的时候,内核会进行如下的处理:

① 在进程的虚拟空间查找一块VMA

② 将这块VMA进行映射。

③ 如果设备驱动程序或者文件系统的file_operations定义了mmap()操作,则调用它。

④ 将这个VMA插入进程的VMA链表中。

有两种建立页表的方法:使用remap_pfn_range函数一次全部建立;或者通过nopage VMA方法每次建立一个页表。本文只考虑第一种情况。

使用remap_pfn_range

remap_pfn_range负责为一段物理地址建立新的页表,它有如下的原型:

int remap_pfn_range(struct vm_area_struct *vma, unsigned long from,

unsigned long to, unsigned long size, pgprot_t prot)

vma:虚拟内存区域,在一定范围内的页将被映射到该区域内。

from:表示内存映射开始处的虚拟地址。

to:虚拟地址应该映射到的物理地址的页帧号。

size:以字节为单位,被重新映射的区域大小。

prot:新页所要求的保护属性。

三、驱动程序代码

#include

#include

#include   /*MKDEV*/

#include   /*printk*/

#include       /*file_operations*/

#include     /*cdev*/

#include       /*vm_area_struct*/

#include       /*PAGE_SHIFT*/

  

#define MAX_SIMPLE_DEV 1

static int simple_major=0;  /*定义主设备号*/

//打开设备文件时被调用

static int simple_open(struct inode *inode,struct file *filp)

{

    return 0;

}

//关闭设备文件时被调用

static int simple_release(struct inode *inode,struct file *filp)

{

    return 0;

}

//VMA打开函数

void simple_vma_open(struct vm_area_struct *vma)

{

    printk(KERN_NOTICE"Simple VMA open,virt %lx,phys %lx\n",vma->vm_start,vma->vm_pgoff<

}

//VMA关闭函数

void simple_vma_close(struct vm_area_struct *vma)

{

    printk(KERN_NOTICE"Simple VMA close.\n");

}

static struct vm_operations_struct simple_remap_vm_ops={

    /*

     *在内核生成一个VMA后,会调用VMAopen()函数,但是,当用户进行mmap()系统调用后,

     *尽管VMA在设备驱动文件操作结构体的mmap()被调用前就已产生,内核却不会调用VMA

     *open()函数,通常需要在驱动的mmap()函数显示的调用

     */

    .open  = simple_vma_open,  

    .close = simple_vma_close,

};

static int simple_remap_mmap(struct file *filp,struct vm_area_struct *vma)

{

    //建立页表

    if(remap_pfn_range(vma,vma->vm_start,vma->vm_pgoff,vma->vm_end-vma->vm_start,vma->vm_page_prot))

        return -EAGAIN;

    //填充VMA结构体中的vm_operations_struct指针

    vma->vm_ops=&simple_remap_vm_ops;

  

    /*

     *simple_vma_open函数的显示调用,

     */

    simple_vma_open(vma);

    return 0;   

}

//定义文件操作结构体

static struct file_operations simple_remap_ops={

    .owner    = THIS_MODULE,

    .open     = simple_open,

    .release  = simple_release,

    .mmap     = simple_remap_mmap,   

};

static void simple_setup_cdev(struct cdev *dev,int minor,struct file_operations *fops)

{

    int err,devno=MKDEV(simple_major,minor);

    //静态初始化cdev

    cdev_init(dev,fops);

    dev->owner=THIS_MODULE;

    

    //注册设备

    err=cdev_add(dev,devno,1);

    if(err)

        printk(KERN_NOTICE"Error %d adding simple%d",err,minor);  

}

static struct cdev SimpleDevs[MAX_SIMPLE_DEV];  /*静态的定义两个设备*/

static int simple_init(void)

{

    int result;

    dev_t dev=MKDEV(simple_major,0);  /*得到设备号*/

    if(simple_major)

        result=register_chrdev_region(dev,2,"simple");   /*静态的分配设备号*/

    else

    {

        result=alloc_chrdev_region(&dev,0,2,"simple");   /*动态的分配设备号*/

        simple_major=MAJOR(dev);                         /*得到主设备号*/

    }

    if(result<0)

    {

        printk(KERN_NOTICE"simple:unable to get major %d\n",simple_major);

        return result;

    }

    //设置两个设备

    simple_setup_cdev(SimpleDevs,0,&simple_remap_ops);

    return 0;   

}

static void simple_cleanup(void)

{

    cdev_del(SimpleDevs);                              /*注销字符设备*/

    unregister_chrdev_region(MKDEV(simple_major,0),2); /*释放设备号*/

}

module_init(simple_init);

module_exit(simple_cleanup);

MODULE_AUTHOR("chenqi");

MODULE_LICENSE("GPL");

   

四、测试程序代码

#include

#include   /*mmap*/

#include      /*stderr*/

int main(int argc,char **argv)

{

    char *fname;

    FILE *f;

    unsigned long offset,len;

    void *address;

 

    //用于判断输入的方式是否正确

    if(argc!=4 || sscanf(argv[2],"%li",&offset)!=1

               || sscanf(argv[3],"%li",&len)!=1)

    {

        fprintf(stderr, "%s: Usage \"%s \"\n", argv[0],

argv[0]);

        exit(1);

    }

    fname=argv[1];   /*表示设备名*/

    if(!(f=fopen(fname,"r")))  /*打开设备文件*/

    {

        fprintf(stderr,"%s:%s:%s\n",argv[0],fname,strerror(errno));

        exit(1);

    }

    //将设备地址映射到用户空间

    address=mmap(0, len, PROT_READ, MAP_FILE | MAP_PRIVATE, fileno(f), offset);

    if(address==(void *)-1)

    {

        fprintf(stderr,"%s: mmap(): %s\n",argv[0],strerror(errno));

        exit(1);

    }

   

    fclose(f);   /*关闭由fopen()函数打开的文件*/

    fprintf(stderr, "mapped \"%s\" from %lu (0x%08lx) to %lu (0x%08lx)\n",

fname, offset, offset, offset+len, offset+len);

   

    //

    fwrite(address, 1, len, stdout);

    return 0;

}

你可能感兴趣的:(Linux,内存,存储,内存映射)