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的用途所在。 

原创粉丝点击