linux 查看内存映射,linux内存映射相关知识点

说明:此文档综合了网上很多文章,并结合自己的分析,综合《情景分析》。里面的代码是网上的,尚未亲自验证,有时间了好好搞搞,若用版权问题,请及时通知,务必删除,谢谢。

1.外设内存资源

44711304_1.gif

通常,在系统运行时,外设的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 

#include 

#include 

#include 

#include 

#include 

#include 

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 

#include 

#include 

#include 

#include  /* formem_map_(un)reserve */

#include  /* for virt_to_phys */

#include  /* 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)) 

{

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

{

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 <

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值