IO端口和IO内存(2)
时间:2010-11-08 来源:怪怪虎
8.4.1 直接映射的内存
几种计算机平台上保留了部分内存地址空间留给 I/O 区域,并且自动禁止对该内存范围内的任何(虚拟)地址进行内存管理。
用在个人数字助理(PDA)中的 MIPS 处理器就是这种配置的一个有趣的实例。两个各为 512 MB 的地址段直接映射到物理地址,对这些地址范围内的任何内存访问都绕过 MMU,也绕过缓存。这些 512 MB 地址段中的一部分是为外设保留的,驱动程序可以用这些无缓存的地址范围直接访问设备的 I/O 内存。
其它平台使用另外的方式提供直接映射的地址段:有些使用特殊的地址空间来解析物理地址(例如,SPARC64 就使用了一个特殊的“地址空间标识符”),还有一些则使用虚拟地址,这些虚拟地址被设置成访问时绕过处理器缓存。
当需要访问直接映射的 I/O 内存区时,仍然不应该直接使用 I/O 指针指向的地址――即使在某些体系结构这么做也能正常工作。为了编写出的代码在各种系统和内核版本都能工作,应该避免使用直接访问的方式,而代之以下列函数。
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
这 些宏用来从 I/O 内存接收 8 位、16位和32位的数据。使用宏的好处是不用考虑参数的类型:参数 address 是在使用前才强制转换的,因为这个值“不清楚是整数还是指针,所以两者都要接收”(摘自 asm-alpha/io.h)。读函数和写函数都不会检查参数 address 是否合法,因为这在解析指针指向区域的同时就能知道(我们已经知道有时它们确实扩展成指针的反引用操作)。
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
类似前面的函数,这些函数(宏)用来写 8 位、16位和32位的数据。
memset_io(address, value, count);
当需要在 I/O 内存上调用 memset 时,这个函数可以满足需要,同时它保持了原来的 memset 的语义。
memcpy_fromio(dest, source, num);
memcpy_toio(dest, source, num);
这两个函数用来和 I/O 内存交换成块的数据,功能类似于 C 库函数 memcpy。
在较新的内核版本中,这些函数在所有体系结构中都是可用的。当然具体实现会有不同:在一些平台上是扩展成指针操作的宏,在另一些平台上是真正的函数。不过作为驱动程序开发人员,不需要关心它们具体是怎样工作的,只要会用就行了。
一些 64 位平台还提供了 readq 和 writeq 用于 PCI 总线上的 4 字(8 字节)内存操作。这个 4 字(quad-word)的命名是个历史遗留问题,那时候所有的处理器都只有 16 位的字。实际上,现在把 32 位的数值命名为 L(长字)已经是不正确的了,不过如果对所有东西都重新命名,只会把事情搞得更复杂。
8.4.3 通过软件映射的 I/O 内存
尽管 MIPS 类的处理器使用直接映射的 I/O 内存,但这种方式在现在的平台中是相当少见的;特别是当使用外设总线处理映射到内存的设备时更是如此。
使用 I/O 内存时最普遍的硬件和软件处理方式是这样的:设备对应于某些约定的物理地址,但是 CPU 并没有预先定义访问它们的虚拟地址。这些约定的物理地址可以是硬连接到设备上的,也可以是在启动时由系统固件(如 BIOS)指定的。前一种的例子有 ISA 设备,它的地址或者是固化在设备的逻辑电路中,因而已经在局部设备内存中静态赋值,或者是通过物理跳线设置;后一种的例子有 PCI 设备,它的地址是由系统软件赋值并写入设备内存的,只在设备加电时才存在。
不管哪种方式,为了让软件可以访问 I/O 内存,必须有一种把虚拟地址赋于设备的方法。这个任务是由 ioremap 函数完成的,我们在“vmalloc 和相关函数”中已有介绍。这个函数因为与内存的使用相关,所以已经在前面的章节中讲解过了,它就是为了把虚拟地址指定到 I/O 内存区域而专门设计的。此外,由内核开发人员实现的 ioremap 在用于直接映射的 I/O 地址时不起任何作用。
一旦有了 ioremap 和 iounmap ,设备驱动程序就能访问任何 I/O 内存地址,而不管它是否直接映射到虚拟地址空间。不过要记住,这些地址不能直接引用,而应该使用象 readb 这样的函数。这些函数定义如下:
#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);
首先,注意新函数 ioremap_nocache。第 7 章中没有具体讲解它,因为它的含义是与硬件相关的。引用内核中的一个头文件的描述:“如果有某些控制寄存器在这个区域,并且不希望发生写操作合并或读缓存 的话,可以使用它。”实际上,在大多数计算机平台上这个函数的实现和 ioremap 是完全一样的:因为在所有 I/O 内存都已经可以通过非缓存地址访问的情况下,就不必实现一个单独的,非缓存的 ioremap 了。
ioremap 的另一个重要特点是在内核 2.0 中它的行为和后来内核中的不同。在 Linux 2.0 中,该函数(那时称为 vremap)不能映射任何没有对齐页边界的内存区。这是个明智的选择,因为在 CPU 一级所有操作都是以页面大小的粒度进行的。但是,有时候需要映射小的 I/O 寄存器区域,而这些寄存器的(物理)地址不是按页面对齐的。为适应这种新需求,内核 2.1.131 及后续版本中允许重映射未对齐的地址.
/* Remap a not (necessarily) aligned port region */
void *short_remap(unsigned long phys_addr)
{
/* The code comes mainly from arch/any/mm/ioremap.c */
unsigned long offset, last_addr, size;
last_addr = phys_addr + SHORT_NR_PORTS - 1;
offset = phys_addr & ~PAGE_MASK;
/* Adjust the begin and end to remap a full page */
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr) - phys_addr;
return ioremap(phys_addr, size) + offset;
}