Linux设备驱动开发详解--笔记11--内存与I/O访问

typedef void (*lpFunction) ();//定义一个无参数,无返回类型的函数指针类型

//定义一个函数指针,指向cpu启动后所执行的第一条指令的位置

lpFunction lpReset = (lpFunction)0xF000FFF0;

lpReset();//调用函数

 

MMU停供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。操作系统内核借助MMU,可以让用户感觉到好像程序可以使用非常大的内存空间,从而使得编程人员在写程序时不用考虑计算机的物理内存的实际容量

 

如图,当ARM要访问存储器时,MMU先查找TLB中的虚拟地址表。如果ARM的结构支持分开的数据TLB(DTLB)和指令TLB(ITLB),则除去指令使用的ITLB外,其他的都是用DTLB

 

若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器中的转换表中获取地址转换信息和访问权限(即执行TTW),同时将这些信息放入TLB,它或者被放在一个没有使用的入口或者替换一个已经存在的入口。之后,在TLB条目中控制信息的控制下,当访问权限允许时,对真实物理地址的访问将在Cache或者在内存中发生

用户空间0-3GB,内核空间3-4GB,用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。用户进程只能通过系统调用等方式才能访问到内核空间

每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的叶表,内核的虚拟空间独立于其他程序

 

Linux中的1GB内核地址空间又被划分为内存映射区,虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区

 

void kmalloc(size_t size, int flags);

第二个参数为分配标志,用于控制kmalloc的行为,最通常用的分配标志是GFP_KERNEL,其含义是再内核空间的进程中申请内存。Kmalloc的底层依赖__get_free_pages()实现,GFP正好是这个函数的缩写,使用GFP_KERNEL标志申请内存时,若暂时不能满足,则进程会睡眠等待页,即引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用此标志申请内存

在中断处理函数、tasklet和内核定时器等非进程上下文中不能阻塞,此时驱动应该使用GFP_ATOMIC标志来申请,若不存在空闲页,直接返回

 

__get_free_pages():

此系列函数包括:

get_zeroed_page(unsigned int flags);

返回一个指向新页的指针并且将该页清零

__get_free_page(unsigned int flags)

该宏返回一个指向新页的指针但是该页不清零

__get_free_pages(unsigned int flags, unsigned int order);

该函数可分配多个页并返回分配内存的首地址,分配的页数是2的order次方,分配的页不清零

 

Virt_to_phys():内核虚拟地址转换为物理地址

Phys_to_virt():物理地址到虚拟地址的转换

 

设备通常会提供一组寄存器来用于控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。这些寄存器可能位于I/O空间,也可能位于内存空间。当位于I/O空间时,通常被称为I/O端口,位于内存空间时,对应的内存空间被称为I/O内存

 

在内核访问I/O内存前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址,该函数返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围。通过ioremap获得的虚拟地址应该被iounmap()函数释放:

Viod *ioremap(unsigned long offset, unsigned long size);

Void iounmap(void *addr);

 

ioport_map函数,可以把port开始的count连续的I/O端口映射为一段“内存空间”。然后就可以再其返回的地址上像访问I/O内存一样访问这些端口。当不再需要这种映射时,需要ioport_unmap函数来撤销

Void *ioport_map(unsigned long port, unsigned int count);

Void ioport_unmap(void *addr);

映射到内存空间行为实际上时给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口访问I/O端口

 

设备驱动访问I/O端口步骤:

一种是直接使用I/O端口操作函数:

在设备打开或驱动模块被加载时申请I/O端口区域,之后使用inb()、outb()等进行端口访问,最后,在设备关闭或驱动呗卸载时释放I/O端口范围,如图:

 

另一种是将IO端口映射为内存进行访问:

在设备打开或驱动模块被加载时,申请IO端口区域并使用ioport_map()映射到内存,之后使用IO内存函数进行端口访问,最后,在设备关闭或驱动别卸载时十二房IO端口并释放映射,如图:

设备驱动访问I/O内存步骤:

首先调用request_mem_region()申请资源,接着讲寄存器地址通过ioremap()函数映射到内核空间虚拟地址,之后就可以通过Linux设备访问编程接口访问这些设备的寄存器了。访问完成后,应该对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的IO内存资源,如图:

设备驱动程序中可以实现mmap函数,可以使得用户空间能直接访问设备的物理地址。实际上mmap函数实现了这样一个映射过程:它将用户空间的一段内存与设备内存关联,当用户空间的这段地址范围时,实际上会转化为设备的访问

 

在讲Linux移植到目标电路板的过程中,通常会建立外设IO内存物理地址到虚拟地址的静态映射,这个映射通过在电路板对应的map_desc结构体数组中添加新的成员来完成

 

Dma是一种无须cpu的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。使用dma可以使系统cpu从实际的io数据传输过程中摆脱出来,从而大大提高系统的吞吐率。Dma通常与硬件体系结构特别是外设的总线技术密切相关

Dma方式的数据传输由dma控制器(dmac)控制,在传输期间,cpu可以并发的执行其他任务。当dma结束后,dmac通过中断通知cpu数据传输已经结束,然后cpu执行相应的中断服务程序进行后处理

 

发生cache与内存不一致性错误后,驱动将无妨正常运行。如果没有相关的背景知识,工程师几乎无法地位错误的原因,因为看起来所有的程序都是完全正确的。

解决由于dma导致的cache的一致性问题的最简单的方法是直接禁止dma目标范围内内存的cache功能。当然这是牺牲性能,但是更可靠

 

基于dma的硬件使用总线地址而非物理地址,总线地址是从设备角度上看到的内存地址,物理地址则是从cpu角度上看到的未经转换的内存地址

 

Linux设备驱动中的dma代码流程,如图:

你可能感兴趣的:(Linux)