几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据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)中,原型如下:
void *ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
ioremap()返回一个特殊的虚拟地址,该地址可用来存储特殊的物理地址范围。
iounmap函数用于取消ioremap()所做的映射,原型如下:
void iounmap(void* addr);
这两个函数都是实现在mm/ioremap.c文件中。在将I/O内存资源的物理地址映射成系统虚拟地址后,理论上讲我们就可以像读写RAM那样直接读写I/O内存资源。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。
一、将设备地址映射到用户空间mmap
我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。在内核驱动程序的初始化阶段,通过ioremap()将物理地址映射到内核虚拟空间;在驱动程序的mmap系统调用中,使用remap_page_range()将该块内存映射到用户空间。这样内核空间和用户空间都能访问这段被映射后的虚拟地址。实际上,mmap()实现了这样的一个映射过程:它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。
mmap()必须以PAGE_SIZE为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行页对齐,强行以PAGE_SIZE的倍数大小进行映射。
从file_operations文件操作结构体可以看出,驱动中mmap()函数的原型如下:
Int (*mmap)(struct file *, struct vm_area_struct*);
VMA 结构体
struct vm_area_struct {
struct mm_struct *vm_mm; /*所处的地址空间 */
unsigned long vm_start; /*开始虚拟地址 */
unsigned long vm_end; /*结束虚拟地址 */
pgprot_t vm_page_prot; /*访问权限 */
unsigned long vm_flags; /*标志,VM_READ/VM_WRITE/VM_EXEC/VM_SHARED等*/
struct vm_operations_struct *vm_ops; /*操作 VMA的函数集指针 */
unsigned long vm_pgoff; /*偏移(页帧号)*/
struct file *vm_file;
void *vm_private_data;
};
驱动程序中mmap函数实现范例:
1. static int xxx_mmap (struct file *filp, struct vm_area_struct *vma)
2. {
3. int err = 0;
4. unsigned long start = vma->vm_start;
5. unsigned long size = vma->vm_end - vma->vm_start;
6. vma->vma_flags|= VM_IO;
7. vma->vma_flags |= VM_RESERVED;
8. err = remap_pfn_range (vma, start, vma->vm_pgoff, size, vma->vm_page_prot);
9. return err;
10. }
(1) 输入输出空间不是普通内存时,在vma->vma_flags上以位操作或(OR)方式给出VM_IO,从而表示内核相应的空间为输入输出内存空间。
vma->vma_flags |= VM_IO;
只有这样,当硬件出错,分配输入输出区域的地址空间失败,也会由处理器发生 coredump,防止结束运行。
(2) 预留空间显示内存特性时,一定要在vma->vma_flags上以位操作或(OR)方式给出VM_RESERVED
vma-> vma_flags |=VM_RESERVED;
mmap函数中关键为调出remap_page_range()函数创建页表,以VMA结构体的成员(VMA的数据成员是内核根据用户的请求自己填充的)作为remap_page_range()的参数,映射的地址范围是vm_start和 vm_end之间。
int remap_pfn_range(struct vm _area_struct * vma, unsignedlong addr,
unsigned long pfn, unsigned long size, pgprot_t prot);
(1) addr参数表示内存映射开始处的虚拟地址。remap_pfn_range()函数为 addr~addr+size之间的虚拟地址构造页表。
(2) pfn是虚拟地址应该映射到的物理地址的页帧号,实际上就是物理地址右移 PAGE_SHIFT位。若PAGE_SIZE为4KB,则PAGE_SHIFT为12,因为PAGE_SIZE等于1<
(3) prot是新页所要求的保护属性。
驱动中的mmap()函数将在用户进行 mmap()系统调用时最终被调用,mmap()系统调用的原型与 file_operations中mmap()的原型区别很大,如下所示:
caddr_t mmap(caddr_t addr, size_t len, int prot, intflags, int fd, off_t offset);
1) 参数 fd为文件描述符,一般由 open()返回,fd也可以指定为!1,此时需指定 flags参数中的MAP_ANON,表明进行的是匿名映射。
2) len是映射到调用用户空间的字节数,它从被映射文件开头offset个字节开始算起,offset参数一般设为 0,表示从文件头开始映射。
3) prot参数指定访问权限,可取如下几个值的“或”:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)和 PROT_NONE(不可访问)。
4) 参数 addr指定文件应被映射到用户空间的起始地址,一般被指定为 NULL,这样,选择起始地址的任务将由内核完成,
5) 而函数的返回值就是映射到用户空间的地址。其类型 caddr_t实际上就是 void *。
由 mmap()系统调用映射的内存可由 munmap()解除映射,这个函数的原型如下:
intmunmap(caddr_t addr, size_t len );
二、直接内存访问DMA
DMA是一种无需 CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。使用 DMA 可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率。DMA通常与硬件体系结构特别是外设的总线技术密切相关。
DMA方式的数据传输由 DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务。当 DMA结束后,DMAC通过中断通知 CPU数据传输已经结束,然后由 CPU执行相应的中断服务程序进行后处理。
1、DMA与Cache一致性
Cache和DMA本身似乎是两个毫不相关的事物。Cache被用做CPU针对内存的缓存,利用程序的空间局部性和时间局部性原理,达到较高的命中率从而避免CPU每次都一定要与相对慢速的内存交互数据来提高数据的访问速率。DMA可以用做内存与外设之间传输数据的方式,这种传输方式之下,数据并不需要经过 CPU中转。假设DMA针对内存的目的地址与Cache 缓存的对象没有重叠区域,DMA和Cache之间将相安无事。但是,如果DMA的目的地址与 Cache所缓存的内存地址访问有重叠,经过DMA操作,Cache缓存对应的内存的数据已经被修改,而CPU本身并不知道,它仍然认为Cache中的数据就是内存中的数据,以后访问Cache 映射的内存时,它仍然使用陈旧的Cache数据。这样就发生Cache与内存之间数据“不一致性”的错误。
2、一致性DMA缓冲区
DMA映射包括两个方面的工作:分配一片DMA缓冲区;为这片缓冲区产生设备可访问的地址。同时,DMA映射也必须考虑Cache一致性问题。内核中提供了以下函数用于分配一个 DMA一致性的内存区域:
void * dma_alloc_coherent(structdevice *dev, size_t size, dma_addr_t * handle, gfp_t gfp);
上述函数的返回值为申请到的 DMA 缓冲区的内存地址,此外,该函数还通过参数 handle 返回DMA缓冲区的总线地址。handle的类型为dma_addr_t,代表的是总线地址。dma_alloc_coherent()申请一片 DMA 缓冲区,进行地址映射并保证该缓冲区的 Cache 一致性。与dma_alloc_coherent()对应的释放函数为:
void dma_free_coherent(structdevice *dev, size_t size, void * cpu_addr, dma_addr_t handle);
以下函数用于分配一个写合并(writecombining)的 DMA 缓冲区:
void * dma_alloc_writecombine(structdevice * dev, size_t size, dma_addr_t * handle, gfp_t gfp);
与dma_alloc_writecombine()对应的释放函数dma_free_writecombine()实际上就是 dma_free_coherent(),因为它定义为:
#define dma_free_writecombine(dev,size,cpu_addr, handle) \
dma_free_coherent(dev,size,cpu_addr, handle)
三、DMA驱动程序
DMA驱动程序中通常需要申请源地址DMA缓冲区和目的地址DMA缓冲区以及释放DMA缓冲区,分别使用dma_alloc_writecombine()和dma_free_writecombine()函数实现。DMA控制器的每个通道都可以无限制的执行系统总线与/或外设总线之间设备的数据移动。换句话说,每个通道都可以处理以下 4 种情况:
1) 源和目标都在系统总线上-----------------------内存间数据拷贝
2) 当目标在外设总线上时源在系统总线上-----外设向内存拷贝数据
3) 当目标在系统总线上时源在外设总线上-----内存向外设拷贝数据
4) 源和目标都在外设总线上-----------------------外设间数据拷贝
DMA驱动程序中通常需要把源地址,目的地址,数据长度告诉DMA控制器,使能DMA中断,以及在DMA中断中唤醒等待DMA数据的进程。
四、内核空间内存动态申请
在Linux内核空间申请内存涉及的函数主要包括 kmalloc()、_ _get_free_pages()和 vmalloc()等。kmalloc()和_ _get_free_pages()(及其类似函数)申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系。而vmalloc()在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,而vmalloc()申请的虚拟内存和物理内存之间也没有简单的换算关系。
(1) kmalloc()
void *kmalloc(size_tsize, int flags);
给kmalloc()的第一个参数是要分配的块的大小,第二个参数为分配标志,用于控制 kmalloc()的行为。
最常用的分配标志是 GFP_KERNEL,其含义是在内核空间的进程中申请内存。
使用kmalloc()申请的内存应使用kfree()释放,这个函数的用法和用户空间的free()类似。
(2) _ _get_free_pages ()
最底层的内存申请总是以页为单位的:
get_zeroed_page(unsignedint flags); 该函数返回一个指向新页的指针并且将该页清零。_ _get_free_page (unsignedint flags);该宏返回一个指向新页的指针但是该页不清零。_ _get_free_pages(unsigned int flags, unsigned intorder);该函数可分配多个页并返回分配内存的首地址,分配的页数为 2order,分配的页也不清零。order允许的最大值是 10(即1024页)或者11(即2048页),依赖于具体的硬件平台。
_ _get_free_pages 等函数在使用时,其申请标志的值与 kmalloc()完全一样,各标志的含义也与kmalloc()完全一致,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。
(3) vmalloc()
vmalloc()一般用在为只存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存,vmalloc()远大于_ _get_free_pages()的开销,为了完成 vmalloc(),新的页表需要被建立。因此,只是调用 vmalloc()来分配少量的内存(如 1 页)是不妥的。
vmalloc()申请的内存应使用 vfree()释放,vmalloc()和 vfree()的函数原型如下:
void*vmalloc(unsigned long size);
void vfree(void *addr);
vmalloc()不能用在原子上下文中,因为它的内部实现使用了标志为GFP_KERNEL的 kmalloc()。使用 vmalloc 函数的一个例子函数是 create_module()系统调用,它利用 vmalloc()函数来获取被创建模块需要的内存空间。