X86处理器中存在I/O空间的概念,I/O空间是相对于内存空间而言的,通过特定的指令in, out 来访问。
目前,大多数嵌入式微控制器如ARM, POWERPC等并不提供I/O空间,而仅存在内存空间。
typedef void (*lpFunction) (); //定义一个无参数,无返回类型的函数指针类型
lpFunction lpReset = (lpFunction) 0xF000FFF0; //定义一个函数指针,指向cpu启动后执行的第一条指令位置。
lpReset();调用函数
以上程序没有定义任何一个函数实体,但是却执行了函数调用lpReset(), 起到了软重启的作用,跳转到cpu启动后第一条要执行的指令的位置。因此可以通过函数指针调用一个没有函数体的”函数“,本质上是换了一个地址开始执行。
内存管理单元MMU
TLB:转换旁路缓存,是MMU的核心部件,它缓存少量的虚拟地址与物理地址的转换关系,是转换表的cache,常称为”块表“
TTW:转换漫游表,当TLB中没有缓冲对应的地址转换关系时,要通过内存中转换表的访问来获得虚拟地址和物理地址的对应关系.TTW成功后,结果写入TLB.
进程能访问的内存达到4GB,它分为两部分0-3为用户空间,3-4为内核空间。
每个进程的用户空间是完全独立、互不相关的,用户进程各自有不同的页表。而内核空间则是固定的。
1GB的内核地址空间从低地址到高地址分别为:物理内存映射区、隔离带,vmalloc虚拟内存分配区、隔离带、高端页面映射区、专用页面映射区和系统保留映射区。
内核空间申请内存主要函数有:kmalloc(), __get_free_pages()他们申请的内存位于物理内存映射区,且在物理上是连续的,与真实的物理地址只有一个固定的偏移。
vmalloc()申请的连续虚拟内存空间在物理上则不一定连续,它们之间也没有简单的换算关系。
kmalloc()常用的标准时GFP_KERNEL,含义是在内核空间中申请内存。使用GFP_KERNEL时若暂时申请得不到满足,则进程会睡眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用。
可以设置为GFP_ATOMIC,它不会阻塞,立即返回。
释放函数kfree()
__get_free_pages()系列函数是kmalloc()实现的基础。包括get_zeros_page(), __get_free_page() 和__get_free_pages()一系列函数。
释放函数free_page(), free_pages()
vmalloc()一般用在只存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存。它的开销很大,需要建立新的页表,因此,只用它来分配少量内存是不妥的. 它不能用在原子上下文。
释放函数vfree().
slab 在操作系统的运作过程中,经常有大量对象的重复生成、使用和释放的问题。slab算法使对象前后两次被使用时分配在同一块内存空间,并保留了基本的数据结构,效率可以大大的提高。
创建slab缓存 struct kmem_cache *kmem_cache_create();
分配slab缓存 kmem_cache_alloc()
释放slab缓存 kmem_cache_free()
收回slab缓存 kmem_cache_destroy();
内存池是一种非常经典的用于分配大量小对象的后备缓存技术。
mempool_create()。。。
virt_to_phys()可以实现内核虚拟地址转化为物理地址。
设备通常会提供一组寄存器用于控制设备、读写设备、和获取设备状态,即控制寄存器、数据寄存器、和状态寄存器。这些寄存器可能位于I/O空间,也可能位于内存空间。当位于I/O空间时,通常称为I/O端口,位于内存空间时,对应的内存空间称为I/O内存。
一系列inx, outx函数可以读写I/O端口。
而I/O内存在访问之前,需要先用ioremap()函数将设备所处的物理地址映射到虚拟地址。
I/O内存的读写用ioreadx(), iowritex()等一系列函数进行。
可以用ioport_map()将连续的I/O端口重映射为一段”内存空间“。这样可以像操作I/O内存一样操作I/O端口。
i/o端口申请 struct resource *request_region(),释放 release_region()
i/o内存申请 struct resource *request_mem_region(), 释放 release_mem_region()
i/o端口访问流程1(直接访问) request_region()(在设备驱动模块加载或open()函数中进行) -> inb()等(在设备初始化, write()等函数中进行) -> release_region()(在卸载或release()函数中进行)
i/o端口访问流程2(映射成内存) request_region -> ioport_map()(前两步在加载或open()函数中调用) -> ioread等 -> ioport_unmap() ->release_region()
i/o内存访问流程 request_mem_region() -> ioremap() -> ioread等 -> iounmap() -> release_mem_region()
mmap()实现了将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,会转化为对设备的访问。
mmap()必须以PAGE_SIZE为单位进行映射。
调用mmap()的时候,内核会进行如下处理:
1,在进程的虚拟空间查找一块VMA
2,将这块VMA进行映射
3,如果设备驱动程序或文件系统的file_operations定义了mmap()操作,则调用它
4,将这个VMA插入到进程的VMA链表中
驱动程序中的mmap()实现机制是建立页表,并填充VMA结构体中vm_operations_struct指针。
针对VMA的操作都被包含在vm_operations_struct结构体中。
在内核生成一个VMA后,他会调用该VMA的open()函数。但是,当用户进行mmap()系统调用后,尽管VMA在设备驱动文件操作结构体的mmap()被调用前就已经产生,内核却不会调用VMA的open()函数,通常需要在驱动的mmap()函数中显示调用vma->vm_ops->open().
vm_operations_struct操作范例
static int xxx_mmap(struct file * filp, struct vm_area_struct *vma)
{
if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot)) //建立页表
return -EAGAIN;
vma->vm_ops = &xxx_remap_vm_ops;
xxx_vma_open(vma);
return 0;
}
remap_pfn_range()可以一次建立页表,能用它映射内存中的保留页和设备I/O内存。 其中pfn参数是虚拟地址应该映射到的物理地址的页帧号,实际上就是物理地址有益PAGE_SHIFT位。若PAGE_SIZE = 4KB, 则PAGE_SHIFT = 12.即1<
kmalloc()申请的内存若要被映射到用户空间,可以通过mem_map_reserve()设置为保留后进行。
范例:
int __init kmalloc_map_init(void) //模块加载
{
...
buffer = kmalloc(BUF_SIZE, GFP_KERNEL);
for(page = virt_to_page(buffer); page < virt_to_page(buffer + BUF_SIZE); page++)
mem_map_reserve(page); //置页为保留
}
//mmap()函数
...
while(size > 0)
{
//每次映射一页
page = virt_to_phys((void*)pos);
if (remap_page_range(start, page, PAGE_SIZE, PAGE_SHARED))
return -EAGAIN;
start += PAGE_SIZE;
pos += PAGE_SIZE;
size -= PAGE_SIZE;
}
...
I/O内存被映射时需要是nocache的,这时我们需要对vma->vm_page_prot设置nocache标志之后再映射。
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);//赋nocache标志。
除remap_pfn_range()外,驱动程序中实现的nopage函数可以为设备提供更加灵活的内存映射途径。当访问的页不在内存,即发生缺页异常时,nopage()会被内核自动调用。缺页异常时,系统会经过如下处理过程:
1,找到缺页的虚拟地址所在的VMA
2,如果必要,分配中间页目录表和页表
3,如果页表项对应的物理页不存在,则调用这个VMA的nopage()方法,它返回物理页面的描述符
4,将物理页面的地址填充到页表中
在将linux移植到目标电路板的过程中,通常会建立外设I/O内存物理地址到虚拟地址的静态映射,这个映射通过在电路板对应的map_desc结构体数组中添加新的成员来完成。
linux移植到特定平台上,MACHINE_START到MACHINE_END宏之间的定义针对特定电路板而设计,其中的map_io()成员函数完成I/O内存的静态映射。
最终调用的是cpu->map_io()建立map_desc数组中物理内存和虚拟内存的静态映射关系。
伺候在设备驱动驱动中访问经过map_desc数组映射后的I/O内存时,直接在map_desc中该段的虚拟地址上加上相应的偏移即可,不再需要使用ioremap().
DMA无需CPU的参与就可以实现外设与系统内存之间进行双向数据传输的硬件机制。
cache被用作CPU针对内存的缓存。
如果DMA的目的地址和cache所缓存的内存地址访问有重叠,经过DMA操作,cache缓存对应的内存的数据已经被修改,而CPU本身不知道,它仍然认为cache中的数据就是内存中的数据,以后访问cache映射的内存时,它仍然使用陈旧的cache数据。这就发生了cache与内存之间数据”不一致性“错误。
对于带MMU功能的ARM处理器,在开启MMU之前需要先置cache无效,TLB也是如此。
内存中用于与外设交互数据的一块区域称作DMA缓冲区。在设备不支持scatter/gather CSG, 分散/聚集操作的情况下,DMA缓冲区必须是物理上连续的。
基于DMA的硬件使用总线地址而非物理地址。总线地址是从设备角度上看到的内存地址,物理地址是从CPU角度上看到的未经转换的内存地址(经过转换的为虚拟地址)。 并不是所有的平台总线地址即为物理地址。
内核提供了虚拟地址/总线地址转换函数:
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned_long_address);
以上函数不建议使用。
IOMMU的工作原理与CPU内的MMU非常类似,不过它针对的是外设总线地址和内存地址之间的转化。
DMA映射包括两个方面的工作:分配一片DMA缓冲区;为这片缓冲区产生设备可访问的地址。同时,DMA映射也必须考虑Cache一致性问题。
dma_alloc_coherent()申请一片DMA缓冲区,进行地址映射并保证该缓冲区的cache一致性。
DMA使用流程: request_dma()并初始化DMAC ->申请DMA缓冲区(以上两步在加载函数或open()函数中进行) -> 进行DMA传输 -> 若使能了对应中断,进行DMA传输后的中断处理 ->释放DMA缓冲区 ->free_dma
I/O内存和I/O端口的访问有一套统一的方法:申请资源 -> 映射 -> 访问 -> 去映射 -> 释放资源
阅读(4287) | 评论(1) | 转发(2) |