Linux的内存映射
在讲解内存映射之前,不得不去探讨Linux内存管理方面的知识。需要说明的是,我们并不需要深入的理解Linux虚拟内存才能去实现Linux的内存映射,所以对于Linux内存管理方面的知识也仅限于最基础的概念。
一、Linux的内存管理
Linux的内存管理子系统是采用请求调页式的虚拟存储器技术实现的,有关虚拟存储器方面的知识可以参考《深入理解计算机系统》第二版的第9章内容,在这里就不做说明。
1、Linux进程的虚拟空间及其划分
在32位硬件平台上,Linux的逻辑地址为32位,因此,每个进程的虚拟地址空间为4GB,在4GB的空间中,操作系统占用了高端的1GB,而低端的3GB则留给用户程序使用。如下图所示:
1) Linux内核虚拟存储器
Linux中1GB的内核虚拟存储器空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域。
一般情况下,物理内存映射区最大长度为896MB,系统的物理内存被顺序映射到物理内存映射区中。当系统物理内存大于896MB时,超过系统物理内存的那部分内存称为高端内存(小于896MB的系统物理内存称为常规内存),内核在存取高端内存时必须将它们映射到高端内存映射区中。下图可以反映出Linux内核虚拟存储器与物理内存之间的映射关系。
注意:物理内存中0~896MB区域通常由内核使用,当然内核不用时用户程序可以使用;896MB以上的区域通常由用户程序来使用。
2) Linux用户虚拟存储器
Linux用户虚拟存储器总是通过页表访问内存,决不会直接访问。如下图所示:
2、进程空间的描述
内核为系统中的每个进程维护一个单独的任务结构task_struct。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID、指向用户栈的指针、可执行目标文件的名字以及程序计数器)。
task_struct中的一个条目指向mm_struct,它描述进程使用的地址空间,我们感兴趣的两个字段是pgd和mmap,其中pgd指向第一级页表(页全局目录)的基址,而mmap指向一个vm_area_struct(区域结构)的链表,每个vm_area_struct结构描述的是进程的一个用户区。如下图所示:
二、Linux的内存映射
当可执行文件准备运行时,可执行文件的内容仅仅映射到了对应进程虚拟地址空间中,而并没有调入物理内存。当程序开始运行并使用到这部分时,Linux才通过缺页中断把它们从磁盘上调入内存。这种将文件连接到进程虚拟地址空间的过程称为内存映射。
1、vm_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_IO和VM_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()实现了这样的一个映射过程,它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。
3、mmap设备操作
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(不可访问)。
flags:MAP_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
#include
#include
#include
#include
#include
#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后,会调用VMA的open()函数,但是,当用户进行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
#include
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
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;
}