linux内存映射相关知识点
来源:互联网 发布:淘宝店铺特效css代码 编辑:程序博客网 时间:2024/05/05 08:43
说明:此文档综合了网上很多文章,并结合自己的分析,综合《情景分析》。里面的代码是网上的,尚未亲自验证,有时间了好好搞搞,若用版权问题,请及时通知,务必删除,谢谢。
1.外设内存资源
通常,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。
2.CPU对外设内存资源的访问
对外部设备的访问有两种不同的形式:
Ø I/O映射方式(I/O-mapped)
典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",这个存储空间与内存分属两个不同的体系,CPU无法通过访问内存的指令而只能通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元;
Ø 内存映射方式(Memory-mapped)
RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。
3.Linux下对外设内存资源的操作
需要注意:
Ø CPU并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围;
Ø Linux下,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访问内存指令访问这些I/O内存资源;
Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,原型如下:
static inline void __iomem * ioremap (unsigned longoffset, unsigned long size){ return__ioremap(offset, size, 0);}
在将I/O内存资源的物理地址映射成虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。但为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。如在x86平台上,读写I/O的函数如下所示:
#define readb(addr) (*(volatile unsigned char *)__io_virt(addr))#define readw(addr) (*(volatile unsigned short *)__io_virt(addr))#define readl(addr) (*(volatile unsigned int *)__io_virt(addr)) #define writeb(b,addr) (*(volatile unsigned char *)__io_virt(addr) = (b))#define writew(b,addr) (*(volatile unsigned short*) __io_virt(addr) = (b))#define writel(b,addr) (*(volatile unsigned int *)__io_virt(addr) = (b)) #define memset_io(a,b,c)memset(__io_virt(a),(b),(c))#define memcpy_fromio(a,b,c)memcpy((a),__io_virt(b),(c))#define memcpy_toio(a,b,c)memcpy(__io_virt(a),(b),(c))
需要注意:
驱动中可能并没有ioremap函数,但是一定存在外设物理地址到内核虚拟地址间的转化过程,达到的效果是一样的。
4.利用mmap()操作设备内存
用mmap映射一个设备,意味着用户空间的一段地址关联到设备内存上,使得用户程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。
参考第7节:样例程序。
5.利用/dev/mem操作设备内存
/dev/mem相当于整个系统的内存(包括系统内存和设备内存和MMIO)的一个映射文件。通过/dev/mem设备文件和mmap系统调用,可以将线性地址描述的物理内存映射到进程 的地址空间,然后就可以直接访问这段内存了。用法一般就是open,然后mmap,接着可以使用map之后的地址来访问物理内存。可作为实现用户空间驱动的一种方法。
两个例子:
1)操作PCI设备
通过/proc/bus/pci获得相应的PCI设备的配置寄存器,再获得相应的物理地址,然后通过调用/dev/mem的mmap方法就可以了。
2)操作显存
比如,标准VGA 16色模式的实模式地址是A000:0000,而线性地址则是A0000。设定显 存大小为0x10000,则可以如下操作
mem_fd = open( "/dev/mem", O_RDWR ); vga_mem = mmap( 0, 0x10000, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, 0xA0000 ); close( mem_fd );
然后直接对vga_mem进行访问就可以了。当然,如果是操作VGA显卡,还要获得I/O 端口的访问权限,以便进行直接的I/O操作,用来设置模式/调色板/选择位面等等 在
3)系统保留内存
这种方法可用来在内核和应用程序之间高效传递数据:
Ø 假定系统有64M物理内存,则可以通过uboot通知内核只使用63M,而保留1M物理内存作为数据交换使用(使用 mem=63M 标记);
Ø 然后打开/dev/mem设备,并将63M开始的1M地址空间映射到进程的地址空间;
样例程序:
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdlib.h>#include <unistd.h>#include <sys/mman.h> int p_addr, v_addr;volatile unsigned char *paddr, *vaddr; int main(int argc, char *argv[]){ int fd; int i, j,va, vd; int bw, cp; charlcmdbuf[128]; char*cmdbuf, ch; p_addr =0x4c000000; if(argc>1){ p_addr = strtoul(argv[1], NULL, 16); } fd =open("/dev/mem", O_RDWR|O_SYNC); if(fd<0){ printf("Error open /dev/mem!"); return -1; } v_addr =(int)mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd,p_addr&~0x00000fff); if(v_addr<0){ printf("Unableto mmap %08x!", p_addr); return-1; }else{ printf("Mappaddr %08x to %08x!", p_addr, v_addr); }; paddr =(char*)p_addr; vaddr =(char*)v_addr; bw = 1; cp = 0; while(1){ if(cp==0){ printf("-"); fgets((char*)lcmdbuf, 128, stdin); } cmdbuf= lcmdbuf+cp; while(*cmdbuf==''){ cmdbuf++; } do{ ch= lcmdbuf[cp]; cp++; }while(ch!=0&& ch!='' && ch!='' && ch!=';'); lcmdbuf[cp-1]= 0; if(ch!=';'){ cp= 0; } if(cmdbuf[0]=='q') break; if(cmdbuf[1]=='b'){ bw= 1; }elseif(cmdbuf[1]=='w'){ bw= 2; }elseif(cmdbuf[1]=='d'){ bw= 4; } switch(cmdbuf[0]){ case'd': sscanf((char*)cmdbuf+2,"%x", &va); for(j=0;j<256; j+=16){ printf("%08x:", (int)paddr+va+j); for(i=0;i<16; i+=bw){ if(bw==1){ printf("%02x", vaddr[va+i+j]); }elseif(bw==2){ printf("%04x", *(unsigned short*)&vaddr[va+i+j]); }elseif(bw==4){ printf("%08x", *(unsigned int*)&vaddr[va+i+j]); } } printf(""); } break; case'r': sscanf((char*)cmdbuf+2,"%x", &va); if(bw==1){ printf("%08x= %02x", (int)paddr+va, *(vaddr+va)); }elseif(bw==2){ printf("%08x= %04x", (int)paddr+va, *(unsigned short*)(vaddr+va)); }elseif(bw==4){ printf("%08x= %08x", (int)paddr+va, *(unsigned int*)(vaddr+va)); } break; case'w': sscanf((char*)cmdbuf+2,"%x %x", &va, &vd); if(bw==1){ *(vaddr+va)= (unsigned char)vd; }elseif(bw==2){ *(unsignedshort*)(vaddr+va) = (unsigned short)vd; }elseif(bw==4){ *(unsignedint*)(vaddr+va) = vd; } break; default: break; } } return 0;}
6. kmalloc()和vmalloc() 函数
kmalloc和vmalloc分配内存最大的不同在于,kmalloc能分配到物理上连续的页,所以kmalloc得到的地址也称为“逻辑地址”(因为是连续的页,所以访问物理内存只需要一个偏移量计算即可,速度快)。系统运行久了以后,连续的地址当然变少,如果在这个时候,分配大片内存,kmalloc得不到满足,而可能需要内核进行移动页面等操作,无益于系统内存的利用和管理。vmalloc分配内存时,不考虑物理内存中是否连续,而使用一个表来转换虚拟地址与物理地址的关系。在分配大内存的时候,vmalloc成功率高,也很好地利用了内存空间。
总之:kmalloc分配到连续的物理内存页,而vmalloc则不连续
7. remap_page_range和remap_vmalloc_range函数
int remap_pfn_range(struct vm_area_struct *vma,unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot);
这个函数就完成“将内核空间的地址与页的对应关系,转化为用户空间中的对应关系”。pfn是Page Frame Number的缩写,即表示一个页的编号。从函数名称便可以看出,它”remap”一个”range”的”pfn”,就是重新映射一个范围的页表。也就是只能映射连续的页。因此这个函数只适用于连续的物理内存页(即kmalloc或者__get_free_pages获得的)
如果不连续的页怎么办?(vmalloc分配的空间)
这种情况可以使用内核提供的vm_operations_struct结构。其结构如下 :
struct vm_operations_struct { void(*open)(struct vm_area_struct * area); void(*close)(struct vm_area_struct * area); int (*fault)(struct vm_area_struct *vma,struct vm_fault *vmf); /* .....*/}
其中的fault原型,指出了内核在找不到某个地址对应的页时,调用的函数。由于页不连续,不能使用remap_pfn_range,即没有建立地址和页的对应关系,所以在MMAP后,用户访问该范围的某地址时,肯定会发生缺页异常,即找不到页!这时会调用fault函数,由驱动来负责寻找这页!怎么找呢?首先,我们可以计算一下,用户试图访问的这个地址,离映射起始地址的偏移 offset;然后,通过这个偏移 offset,我们可以得到内核空间中的地址(通过与vmalloc得出的地址相加即可);最后,通过vmalloc_to_page函数,得到我们找到的内核虚拟地址对应的页表。这就是这个用户地址所对应的页表。
示例代码:
void * kernel_space_addr; /* 将来在某地分配 */unsigned long kernel_space_size; /* 指定分配空间的大小 */ static int vma_fault(struct vm_area_struct *vma,struct vm_fault *vmf) { unsignedlong offset; void *our_addr; offset =(unsigned long)vmf->virtual_address - (unsignedlong)vma->vm_start; /* 计算PAGE_FAULT时的偏移量 */ if(offset >= kernel_space_size) { return -VM_FAULT_NOPAGE; } /* 这次是真的页错误了 */ our_addr= kernel_space_addr + offset; /* 得到该偏移量下的内核虚拟地址 */ vmf->page = vmalloc_to_page(our_addr); /* 将得到的页面告知内核 */ get_page(vmf->page); /* 别忘了增加其引用计数 */ return0;} static const struct vm_operations_struct vmops = { .fault = vma_fault,}; int mmap(struct file *file, struct vm_area_struct*vma) { vma->vm_ops = &vmops; /* 指定vm_ops */ vma->vm_flags |= VM_RESERVED; /* 声明这片内存区不能交换! */ return 0;}
不连续页的另外一个实现:remap_vmalloc_range
这是2.6.18后的内核版本实现的,使用方法也很简单:
int remap_vmalloc_range(structvm_area_struct *vma, void *addr,unsigned long pgoff);
8.示例程序
在内核驱动程序的初始化阶段,通过ioremap()将物理地址映射到内核虚拟空间;在驱动程序的mmap系统调用中,使用remap_page_range()将该块ROM映射到用户虚拟空间。这样内核空间和用户空间都能访问这段被映射后的虚拟地址。
/************mmap_ioremap.c**************/#include <linux/module.h>#include <linux/kernel.h>#include <linux/errno.h>#include <linux/mm.h>#include <linux/wrapper.h> /* formem_map_(un)reserve */#include <asm/io.h> /* for virt_to_phys */#include <linux/slab.h> /* for kmalloc andkfree */ MODULE_PARM(mem_start, "i");MODULE_PARM(mem_size, "i"); static int mem_start = 101, mem_size = 10;static char *reserve_virt_addr;static int major; int mmapdrv_open(struct inode *inode, struct file*file);int mmapdrv_release(struct inode *inode, structfile *file);int mmapdrv_mmap(struct file *file, structvm_area_struct *vma); static struct file_operations mmapdrv_fops ={ owner:THIS_MODULE,mmap: mmapdrv_mmap,open: mmapdrv_open,release: mmapdrv_release,}; int init_module(void){ if ((major =register_chrdev(0, "mmapdrv", &mmapdrv_fops)) < 0) { printk("mmapdrv:unable to register character device/n"); return ( - EIO); } printk("mmapdevice major = %d/n", major); printk("highmemory physical address 0x%ldM/n", virt_to_phys(high_memory) /1024 / 1024); reserve_virt_addr= ioremap(mem_start *1024 * 1024, mem_size *1024 * 1024); printk("reserve_virt_addr= 0x%lx/n", (unsigned long)reserve_virt_addr); if(reserve_virt_addr) { int i; for (i = 0; i< mem_size *1024 * 1024; i += 4) { reserve_virt_addr[i]= 'a'; reserve_virt_addr[i+ 1] = 'b'; reserve_virt_addr[i+ 2] = 'c'; reserve_virt_addr[i+ 3] = 'd'; } } else { unregister_chrdev(major,"mmapdrv"); return - ENODEV; } return 0;} /* remove the module */void cleanup_module(void){ if(reserve_virt_addr) iounmap(reserve_virt_addr); unregister_chrdev(major,"mmapdrv"); return ;} int mmapdrv_open(struct inode *inode, struct file*file){ MOD_INC_USE_COUNT; return (0);} int mmapdrv_release(struct inode *inode, structfile *file){ MOD_DEC_USE_COUNT; return (0);} int mmapdrv_mmap(struct file *file, structvm_area_struct *vma){ unsigned longoffset = vma->vm_pgoff << PAGE_SHIFT; unsigned longsize = vma->vm_end - vma->vm_start; if (size >mem_size *1024 * 1024) { printk("sizetoo big/n"); return ( -ENXIO); } offset = offset +mem_start * 1024 * 1024; /* we do not wantto have this area swapped out, lock it */ vma->vm_flags|= VM_LOCKED; if(remap_page_range(vma, vma->vm_start, offset, size, PAGE_SHARED)) { printk("remappage range failed/n"); return - ENXIO; } return (0);}
关于high_memory:
linux内核规定,只映射0-896M物理内存(如果有的话,称为low memory)到内核空间,也就是0xc0000000+0到0xc0000000+896M,而且是线性映射,有物理地址0<=x<=896M,就有内核地址0xc0000000+x。
如果物理内存y<896M,则high_memory=0xc0000000+y(是虚拟地址);否则high_memory=0xc0000000+896M 或者说high_memory不能超过0xc0000000+896M
//high_memory =(void *) __va(max_low_pfn * PAGE_SIZE);
所以内核情景分析上说high_memory是“具体物理内存的上限对应的虚拟地址”。
如果内核空间需要虚拟空间,就在high_memory+8m分配 。源码中留一个8MB的空洞,以及在每次分配虚存区间时也要留下一个页面的空洞,是为了便于捕捉可能的越界访问。
9.内存映射
内存映射并非映射文件内容到内存中,他的最终目的是提供访问某段物理内存的一种途径,其过程是构造访问这段物理内存的对应的页表项。如果在内核空间来映射,是在内核空间(3G以上)构造页表项,来指向相应的物理内存,例如ioremap目标就是把设备内存的物理地址填到内核页表中,推而广之,kmalloc/vmalloc等也可以算是是一种内存映射,说来其实与ioremap目标一样,只不过后者物理介质是系统内存,前者是设备内存。如果在用户空间映射,是在用户进程地址空间(3G以下)来构造页表指向欲访问的物理地址,这个物理地址可能是设备内存,也可能是内核空间分配的内存(kmalloc/vmalloc),却想在用户空间访问。在用户空间来映射,根据页表构造的途径的不同,又有两种途径,一种是物理地址连续的,这样就可以一次搞定(通过remap_page_range),如果物理地址不连续(多个不连续的物理页面),如果不怕麻烦,可以把这些页面的物理地址都一个个找出来,然后在填到页表项中,这算一种不lazy的方法,似乎也很少用。lazy的方法就是通过缺页异常做,这也就是vm_operations_struct中fault的用途所在。
- linux内存映射相关知识点
- 内存映射相关函数
- 内存映射相关函数
- 内存映射相关学习
- Linux内存管理和进程调度相关知识点
- linux相关的知识点
- linux相关知识点
- linux 相关知识点
- 内存映射文件操作相关
- C程序内存相关知识点
- Linux内存映射: mmap
- Linux 内存映射
- Linux内存映射: mmap
- Linux内存映射
- LINUX 内存映射
- Linux进程内存映射
- linux mmap 内存映射
- linux-内存映射mmap
- MBP本,完美多分区装双系统图文教程,多分区而不影响苹果系统GUID分区表,图文教程
- 文件同步
- 乙肝大三阳恶化有哪些症状
- ResultSetMetaData
- 太阳能发电
- linux内存映射相关知识点
- android 技术细节整理
- chmod 4755和chmod 755的区别
- Java toLowerCase()/toUpperCase()方法的使用注意
- mfs权威指南(moosefs)分布式文件系统一站式解决方案(部署,性能测试)不断更新
- 使用Google的开源TCMalloc库,提高MySQL在高并发情况下的性能
- linux 下vim的安装方法 及一些基本命令
- linux 统计 程序运行时间
- 2011/8/29日志