Linux设备驱动之内存映射
来源:互联网 发布:linux运维需要会什么 编辑:程序博客网 时间:2024/05/10 00:44
1. 内存映射
所谓的内存映射就是把物理内存映射到进程的地址空间之内,这些应用程序就可以直接使用输入输出的地址空间,从而提高读写的效率。Linux提供了mmap()函数,用来映射物理内存。
在驱动程序中,应用程序以设备文件为对象,调用mmap()函数,内核进行内存映射的准备工作,生成vm_area_struct结构体,然后调用设备驱动程序中定义的mmap函数。
2. 映射的种类
把同一个物理地址映射为虚拟地址有两种方法:
1) 第一种是mmap()函数将物理地址映射到进程的虚拟地址空间中去
2)第二种方法为ioremap()函数映射到内核虚拟地址上的方法。
3. 应用程序中的mmap函数
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
start 映射到进程空间的虚拟地址,通常为NULL
length 映射空间的大小
prot 映射到内存的读写权限
flags flags可取MAP_SHARED,MAP_PRIVATE,MAP_FIXED,如果是MAP_SHARED,此进程对映射空间的内容修改会影响到其它的进程,即对其它的进程可见,而MAP_PRIVATE,此进程修改的内容对其它的进程不可见
fd 要映射文件的文件标识符
offset 映射文件的位置,一般从头开始。而在设备文件中,表示映射物理地址的起始地址
4.设备驱动程序的mmap函数
int XXX_mmap(struct file*filp,struct vm_area_struct *vma);
首先调用应用程序的mmap函数,然后内核进行适当处理之后,进行相应的内存映射,即生成vm_area_struct结构体,然后传递给设备驱动程序的mmap函数。
关于vma中的一些参数说明:
(1)unsigned long vm_start 映射到进程空间的起始地址
(2)unsigned long vm_end 映射到进程空间的结束地址
(3)unsigned long vm_flags 即包含在应用程序中mmap中的flags值, 如VM_READ,VM_WRITE,VM_SHARED,VM_EXEC
(4)unsigned long vm_pgoff 映射到物理内存的偏移量
5.mmap映射的方法
有两种方法建立页表:
1) 一次性建立页表,可以调用函数remap_pfn_range
2)每次建立一个页的页表,调用函数nopage。
remap_pfn_range:
这个函数的功能是一次性建立新的页表去映射物理地址。
int remap_pfn_range(struct vma_area_struct* vma,unsigned long virt_addr,unsigned long pfn,unsigned long size,pgprot_t prot);
返回值:映射成功时返回0,否则返回一个错误的负数代码。
vma 物理地址被映射到的虚拟内存区域
virt_addr 被映射到进程空间的起始虚拟地址。页表建立的范围在virt_addr到virt_addr+size
pfn 对应物理地址的页框号,一般是vma->vm_pgoff域。
size 被映射区域的字节大小
prot vma->vm_page_prot
nopage:
struct page *(*nopage) (struct vm_area_struct *vma, unsigned long address, int *type);
vm_area_struct: 虚拟内存区域
address: 发生page fault的进程空间的虚拟地址
type page fault的处理类型
get_page(struct page* pageptr);
增加被映射页的使用次数。
remap_pfn_range与nopage的区别
(1)remap_pfn_range一次性建立页表,而nopage通过缺页中断找到内核虚拟地址,然后通过内核虚拟地址找到对应的物理页
(2)remap_pfn_range函数只对保留页和物理内存之外的物理地址映射,而对常规RAM,remap_pfn_range函数不能映射,而nopage函数可以映射常规的RAM。
4. 例子
下面的例子分别采用remap_pfn_range与nopage建立内存映射
驱动程序memap.c:
#include <linux/module.h>#include <linux/kernel.h>#include <linux/errno.h>#include <linux/mm.h>#include <linux/fs.h>#include <linux/types.h>#include <linux/fcntl.h>#include <asm/uaccess.h>#include <asm/page.h>#include <asm/io.h>#include <linux/vmalloc.h>#include <linux/slab.h>#include <linux/init.h>#define SHARE_MEM_COUNT 4#define SHARE_MEM_SIZE (PAGE_SIZE*SHARE_MEM_COUNT) MODULE_LICENSE("GPL");static char* reserve_virt_addr;static int major;char* share_memory=NULL;int mmapdrv_open(struct inode*,struct file* filp); //驱动程序的open函数int mmapdrv_release(struct inode*,struct file* filp);//驱动程序的release函数int mmapdrv_mmap(struct file* file,struct vm_area_struct*vma);//驱动程序中的mmap函数void simple_vma_open(struct vm_area_struct* vma );//vm_operations_struct对vma(虚拟内存区域)打开函数void simple_vma_close(struct vm_area_struct *vma);//vma的关闭函数struct page* simple_vma_nopage(struct vm_area_struct* vma,unsigned long address,int *type);//nopage映射struct page* simple_vma_nopage1(struct vm_area_struct* vma,unsigned long address,int *type);static int simple_nopage_mmap(struct file* filp,struct vm_area_struct *vma);//驱动程序中的mmap与上面的mmap可选择其一static struct file_operations mmapdrv_fops={ owner:THIS_MODULE, mmap:simple_nopage_mmap, open: mmapdrv_open, release:mmapdrv_release,};static struct vm_operations_struct simple_remap_vm_ops={ .open=simple_vma_open, .close=simple_vma_close, .nopage=simple_vma_nopage1,};static int __init memc_init(void){if((major=(register_chrdev(0,"mapdrv",&mmapdrv_fops)))<0){ printk("register mapdrv failure/n"); return -EIO;}printk("register success,major=%d/n",major);share_memory=vmalloc(SHARE_MEM_SIZE);//通过vmalloc分配内存,返回的是内核虚拟地址,然后将物理内存映射到进程的虚拟地址空间上去int lp;for(lp=0;lp<SHARE_MEM_COUNT;lp++){sprintf(share_memory+PAGE_SIZE*lp,"Test %d",lp);} return 0;}static void __exit memc_exit(void){ if(reserve_virt_addr){ iounmap(reserve_virt_addr);} unregister_chrdev(major,"mapdrv"); return; }int mmapdrv_open(struct inode* inode,struct file* filp){ //MOD_INC_USE_COUNT; return 0;}int mmapdrv_release(struct inode* inode,struct file* filp){ //MOD_DEC_USE_COUNT; return(0);} //remap_pfn_range一次性建立页表进行映射int mmapdrv_mmap(struct file* filp,struct vm_area_struct *vma){ printk("vm_pgoff=0x%lx/n",vma->vm_pgoff<<PAGE_SHIFT); //返回的是物理地址偏移printk("vm_start=0x%lx/n",vma->vm_start);//进程地址空间的起始地址printk("vm_end=0x%lx/n",vma->vm_end);//进程地址空间的结束地址printk("vm_flags=0x%lx/n",vma->vm_flags);unsigned long physical=vma->vm_pgoff<<PAGE_SHIFT;unsigned long size=vma->vm_end-vma->vm_start; //映射的空间长度vma->vm_flags|=VM_RESERVED;//remap只能对VM_RESERVED和物理内存之外的内存进行映射vma->vm_flags|=VM_IO;if(remap_pfn_range(vma,vma->vm_start,vma->vm_pgoff,size,PAGE_SHARED)){ printk("remap page range failed/n");return -ENXIO;}printk("remap page range success/n");return 0; } //nopage映射static int simple_nopage_mmap(struct file* filp,struct vm_area_struct *vma){unsigned long offset=vma->vm_pgoff<<PAGE_SHIFT;if(offset>=__pa(high_memory)||(filp->f_flags&O_SYNC)){ vma->vm_flags|=VM_IO; }vma->vm_flags|=VM_RESERVED;vma->vm_ops=&simple_remap_vm_ops;//当发生page fault时会调用nopage函数进行缺页处理simple_vma_open(vma);return 0;}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<<PAGE_SHIFT);}void simple_vma_close(struct vm_area_struct *vma){printk(KERN_NOTICE "Simple VMA close./n");} //simple_vma_nopage是通过物理地址找到page,而simple_vma_nopage1通过内核虚拟地址找到page, 即vmalloc返回的内核虚拟地址struct page* simple_vma_nopage(struct vm_area_struct* vma,unsigned long address,int *type){printk("call nopage method/n");struct page* pageptr;unsigned long offset=vma->vm_pgoff<<PAGE_SHIFT;unsigned long physaddr=address-vma->vm_start+offset; //address是缺页进程地址空间的虚拟地址,vm_start是进程地址空间的起始映射地址,address-vma->vm_start+offset要映射的物理地址unsigned long pageframe=physaddr>>PAGE_SHIFT;printk("offset is %lx, physaddr is %lx,pageframe is %lx/n",offset,physaddr,pageframe);if(!pfn_valid(pageframe)) return NOPAGE_SIGBUS;pageptr=pfn_to_page(pageframe);//根据页框号,得到pageif(type)*type=VM_FAULT_MINOR;printk("pageptr is %lx/n",(unsigned long)pageptr);return pageptr;} //首先根据page fault的进程地址空间的address找到内核虚拟地址,然后根据内核虚拟地址,即vmalloc返回的地址找到相对应的page//对于address-vma->vm_start地址范围的内容是通过vmalloc()+address-vma->vm_start找到相应的页取得的,所以对于用户空间address-vma->vm_start存储的内容就是vmalloc()+address-vma->vm_start对应页的内容struct page* simple_vma_nopage1(struct vm_area_struct* vma,unsigned long address,int *type){struct page *page;unsigned long offset1;void *page_ptr;unsigned long offset=vma->vm_pgoff<<PAGE_SHIFT;unsigned long physaddr=address-vma->vm_start+offset;unsigned long pageframe=physaddr>>PAGE_SHIFT;printk("vm_pgoff is %lx, PAGE_SHIFT is %lx,PAGE_SIZE is %lx,offset is %lx, physaddr is %lx,pageframe is %lx/n",vma->vm_pgoff,PAGE_SHIFT,(unsigned long)PAGE_SIZE,offset,physaddr,pageframe);offset1=address-vma->vm_start; //映射的进程地址空间的偏移if(offset1>=SHARE_MEM_SIZE)return NOPAGE_SIGBUS;page_ptr=share_memory+offset1;//对应的缺页的内核虚拟地址printk("address is %lx,vma->vm_start is %lx,offset1 is %lx,share_memory is %lx,page_ptr is %lx/n",address,vma->vm_start,offset1,(unsigned long )share_memory,(unsigned long)page_ptr);page=vmalloc_to_page(page_ptr);get_page(page);//增加该页的使用计数if(type) *type=VM_FAULT_MINOR;return page;}module_init(memc_init);module_exit(memc_exit);
测试程序:
test.c
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/ioctl.h>#include <fcntl.h>#include <unistd.h>#include <sys/mman.h>#define SHARE_MEM_COUNT 4#define SHARE_MEM_SIZE (4096*SHARE_MEM_COUNT)int main(){int fd;char *data;int loop;fd=open("/dev/mapdrv",O_RDWR|O_NDELAY);if(fd>=0){data=(char*)mmap(0,SHARE_MEM_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);printf("data is %lx/n",(unsigned long)data);printf("[%s]/n",data+4096*loop);}munmap(data,SHARE_MEM_SIZE);close(fd);}return 0;}
运行:
(1)将memap.c与test.c文件放到/usr/src/kernels/linux-2.6.20/drivers/char目录下。
并在Makefile文件中添加obj-m +=memap.o
(2)返回到/usr/src/kernels/linux-2.6.20下make.
(3)插入模块insmod memap.ko,然后 mknod /dev/globalvar c 252 0
252是动态生成的major值。
(4)编译test gcc -o test test.c
(5)./test,可以打印出写入的值。
[Test 0]
[Test 1]
[Test 2]
[Test 3]
总结:
1.对于mmap的内存映射,是将物理内存映射到进程的虚拟地址空间中去,那么进程对文件的访问就相当于直接对内存的访问,从而加快了读写操作的效率。在这里,remap_pfn_range函数是一次性的建立页表,而nopage函数是根据page fault产生的进程虚拟地址去找到内核相对应的逻辑地址,再通过这个逻辑地址去找到page。完成映射过程。remap_pfn_range不能对常规内存映射,只能对保留的内存与物理内存之外的进行映射。
2.在这里,要分清几个地址,一个是物理地址,这个很简单,就是物理内存的实际地址。第二个是内核虚拟地址,即内核可以直接访问的地址,如kmalloc,vmalloc等内核函数返回的地址,kmalloc返回的地址也称为内核逻辑地址。内核虚拟地址与实际的物理地址只有一个偏移量。第三个是进程虚拟地址,这个地址处于用户空间。而对于mmap函数映射的是物理地址到进程虚拟地址,而不是把物理地址映射到内核虚拟地址。而ioremap函数是将物理地址映射为内核虚拟地址。
3.用户空间的进程调用mmap函数,首先进行必要的处理,生成vma结构体,然后调用remap_pfn_range函数建立页表。而用户空间的mmap函数返回的是映射到进程地址空间的首地址。所以mmap函数与remap_pfn_range函数是不同的,前者只是生成mmap,而建立页表通过remap_pfn_range函数来完成。
- Linux设备驱动之内存映射
- Linux设备驱动之内存映射
- Linux设备驱动之内存映射
- Linux设备驱动之内存映射
- Linux设备驱动之内存映射
- Linux设备驱动之内存映射
- Linux设备驱动之内存映射
- Linux设备驱动之内存映射--mmap--转
- Linux设备驱动之内存映射--mmap--转 .
- Linux设备驱动之内存管理
- Linux块设备驱动之内存模拟块设备
- Linux设备驱动编程之内存与I/O操作
- Linux设备驱动编程之内存与I/O操作
- Linux设备驱动编程之内存与I/O操作
- Linux设备驱动编程之内存与I/O操作
- Linux设备驱动编程之内存与I/O操作
- Linux设备驱动编程之内存与I/O操作
- Linux设备驱动编程之内存与IO操作
- 变量名和地址的关系探讨
- ubuntu下安装与卸载软件方法
- 如何获取结构体某成员的偏移地址
- ORA-16038
- 黑马程序员_JAVA基础之末尾
- Linux设备驱动之内存映射
- hdu 4336 Card Collector 概率DP 状态压缩DP
- 内核双机调试
- 这样用Google
- AndEngine环境配置及extension,AndEngineExamples导入
- Judges' Time Calculation (周赛模拟题)
- 【VB/.NET】Converting VB6 to VB.NET 【Part I】【之一】
- session管理(二)
- 【C语言】指向指针的指针 char * *使用技巧