linux2.4.19下__ioremap和get_vm_area的粗略理解
文章来源:http://gliethttp.cublog.cn
对于提供了MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB.进程的4GB内存空间被人为的分为两个部分--用户空间与内核空间.用户空间地址分布从0到3GB(PAGE_OFFSET,在at91rm9200中它等于0xC0000000),3GB到4GB为内核空间. 内核空间中,从3G到VMALLOC_START这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如我们使用的vmware虚拟系统内存是160M,那么3G~3G+160M这片内存就应该映射物理内存.在物理内存映射区之后,就是vmalloc区域.对于160M的系统而言,VMALLOC_START位置应在3G+160M附近(在物理内存映射区与VMALLOC_START期间还存在一个8M的hole来防止跃界),VMALLOC_END的位置接近4G(最后位置系统会保留AT91C_IO_VIRT_BASE~0xFFFFFFFF作为at91rm9200的IO寄存器操作专用,实际过程是将0xFFFA0000 .. 0xFFFFFFFF寄存器物理地址,线性映射到0xFEFA0000 .. 0xFF000000虚拟地址,所以AT91C_IO_VIRT_BASE=0xFEFA0000,以后直接使用#define AT91_IO_P2V(x) ((x) - AT91C_IO_PHYS_BASE + AT91C_IO_VIRT_BASE);就可以将物理地址直接线性的转换为虚拟地址). kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址,转换过程只是将虚拟地址减去3G,与之对应的函数为phys_to_virt(),将内核物理地址转化为虚拟地址. 而vmalloc申请的内存则位于VMALLOC_START~VMALLOC_END之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续 几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址.根据CPU体系结构的不同,CPU对IO端口的编址方式有两种: (1)I/O映射方式(I/O-mapped) 典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元. (2)内存映射方式(Memory-mapped) RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分.此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令. 但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源. 一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定.但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源.Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中.
1.结构体 1.1>struct vm_struct { unsigned long flags; void * addr; unsigned long size; struct vm_struct * next; }; 2.驱动程序引用 fb_info.RegAddr = (unsigned char*) ioremap_nocache(S1D_PHYSICAL_REG_ADDR,S1D_PHYSICAL_REG_SIZE);
#define ioremap_nocache(off,sz) __arch_ioremap((off),(sz),1) #define __arch_ioremap(off,sz,nocache) \ ({ \ unsigned long _off = (off), _size = (sz); \ void *_ret = (void *)0; \ if (iomem_valid_addr(_off, _size)) \ _ret = __ioremap(iomem_to_phys(_off),_size,0); \ _ret; \ }) #define iomem_valid_addr(iomem,size) (1) #define iomem_to_phys(iomem) (iomem)
所以ioremap_nocache函数最后还是等效于 直接调用 __ioremap函数
3.__ioremap函数分析[/arch/arm/mm/Ioremap.c] void * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags) { void * addr; struct vm_struct * area; unsigned long offset, last_addr; //size=0或者32位数据wraparound了将返回null last_addr = phys_addr + size - 1; if (!size || last_addr < phys_addr) return NULL; //#define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&PAGE_MASK) //页面对齐,最后经过页规整之后,size的值将是页大小的整倍数,即:size % PAGE_SIZE将等于0 offset = phys_addr & ~PAGE_MASK; phys_addr &= PAGE_MASK; size = PAGE_ALIGN(last_addr) - phys_addr;
area = get_vm_area(size, VM_IOREMAP); if (!area) return NULL; addr = area->addr; if (remap_area_pages(VMALLOC_VMADDR(addr), phys_addr >> PAGE_SHIFT, size, flags)) {//remap_area_pages待读 vfree(addr); return NULL; } return (void *) (offset + (char *)addr); } //#define __va(x) ((void *)__phys_to_virt((unsigned long)(x)))
//#define TASK_SIZE (0xc0000000) 3GB //#define AT91_SDRAM_BASE (0x20000000) sdram起始地址
//#define PAGE_OFFSET TASK_SIZE //#define PHYS_OFFSET (AT91_SDRAM_BASE) //#define __virt_to_phys(vpage) ((vpage) - PAGE_OFFSET + PHYS_OFFSET)//用来线性转换3GB~VMALLOC_START-8M虚拟内存地址到物理内存SDRAM地址 //#define __phys_to_virt(ppage) ((ppage) + PAGE_OFFSET - PHYS_OFFSET)//将物理内存AT91_SDRAM_BASE~meminfo.end地址线性转换成3GB~VMALLOC_START-8M的虚拟地址
//在arch/arm/mm/Init.c的void __init mem_init(void)中有high_memory = (void *)__va(meminfo.end);这么一句, //在arch/arm/kernel/Setup.c的void __init setup_arch(char **cmdline_p)命令行解释函数中,调用函数: //parse_cmdline(struct meminfo *mi, char **cmdline_p, char *from);实现了meminfo的填充,其中如果用户通过uboot之类 //的参数mem=31M方式传递了内存大小,那么由linux上电默认的内存大小32M将会被mem=31M代替,这样最后的1M物理内存就被闲置了,可以 //作DMA专用,也可以通过参数@0x20000000指定外部sdram的起始地址,一般不需要,linux上电默认的地址就是sdram的起始地址了.
//#define VMALLOC_OFFSET (8*1024*1024) //#define VMALLOC_START (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1)) //#define VMALLOC_VMADDR(x) ((unsigned long)(x)) //#define VMALLOC_END (AT91C_IO_VIRT_BASE-1) //从上面可以看出vmalloc的VM区域VMALLOC_START是从high_memory+8M空间以后开始的,high_memory到VMALLOC_START之间的8M内存漏洞系统用来捕获 //任何超过内存区域的访问操作,vmalloc()函数多创建的最后一个4k空间漏洞也是出于同样的考虑. struct vm_struct * get_vm_area(unsigned long size, unsigned long flags) { unsigned long addr; struct vm_struct **p, *tmp, *area; area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL); if (!area) return NULL; //人为的多申请4k空间作为hole,用来捕获内存越界异常 size += PAGE_SIZE; addr = VMALLOC_START; write_lock(&vmlist_lock); //vmlist是按虚拟地址从低到高的顺序链接成的扫描链 //这部分的实现方式和上一篇文章《linux下request_mem_region的粗略理解》[http://blog.chinaunix.net/u1/38994/showart_324267.html]异曲同工 for (p = &vmlist; (tmp = *p) ; p = &tmp->next) { if ((size + addr) < addr) goto out; if (size + addr <= (unsigned long) tmp->addr) break; addr = tmp->size + (unsigned long) tmp->addr; if (addr > VMALLOC_END-size) goto out; } //按虚拟地址从低到高的顺序,插入area自身,挤占原有*p. area->flags = flags; area->addr = (void *)addr; area->size = size; area->next = *p; *p = area; write_unlock(&vmlist_lock); return area; out: write_unlock(&vmlist_lock); kfree(area); return NULL; }
|