分类: LINUX
2010-11-08 22:36:22
8.1 I/O 端口和 I/O 内存
每种外设都通过读写寄存器进行控制。大部分外设都有几个寄存器,不管是在内存地址空间还是在 I/O 地址空间,这些寄存器的访问地址都是连续的。
在硬件级上,内存区域和 I/O 区域没有概念上的区别:它们都通过向地址总线和控制总线发送电平信号进行访问(比如读和写信号)*,再通过数据总线读写数据。
注:并非所有的计算机平台都使用读和写信号;有些使用不同的方式处理外部电路。不过这些区别对软件是无关的,为简化讨论,这里假定所有平台都用读和写信号。
一些 CPU 制造厂商在它们的芯片中使用单一地址空间,另一些则为外设保留了独立的地址空间以便和内存区分开来。一些处理器(主要是 x86 家族的)还为 I/O 端口的读和写使用分离的连线,并且使用特殊的 CPU 指令访问端口。
因为外设要与外围总线相匹配,而最流行的 I/O 总线是基于个人计算机模型的,所以即使原本没有独立的 I/O 端口地址空间的处理器,在访问外设时也要虚拟成读写 I/O 端口。这通常是由外部芯片组或 CPU 核心中的附加电路来实现的。后一种方式只在嵌入式的微处理器中比较多见。
基于同样的原因,Linux 在所有的计算机平台上都实现了 I/O 端口,包括使用单一地址空间的 CPU 在内。端口操作的具体实现则依赖于宿主计算机的特定模型和制造了(因为不同的模型使用不同的芯片组把总线操作映射到内存地址空间)。
即使外设总线为 I/O
端口保留了分离的地址空间,也不是所有设备都会把寄存器映射到 I/O 端口。ISA 设备普遍使用 I/O 端口,大多数 PCI
设备则把寄存器映射到某个内存地址区段。这种 I/O 内存通常是首选方案,因为不需要特殊的处理器指令;而且 CPU
核心访问内存更有效率,访问内存时,编译器在寄存器分配和寻址方式选择上也有更多的自由。
>>>> 以上说明了IO端口和IO内存存在的原因。<<<<
8.1.1 I/O 寄存器和常规内存
尽管硬件寄存器和内存非常相似,程序员在访问 I/O 寄存器的时候必须注意避免由于 CPU 或编译器不恰当的优化而改变预期的 I/O 动作。
I/O 寄存器和 RAM 的最主要区别就是 I/O 操作具有边际效应,而内存操作则没有:内存写操作的唯一结果就是在指定位置存储一个数值;内存读操作则仅仅返回指定位置最后一次写入的数值。由于内存访问 速度对 CPU 的性能至关重要,而且也没有边际效应,所以可用多种方法进行优化,如使用高速缓存保存数值,重新排序读/写指令等。
编译器能够将数值缓存在 CPU 寄存器中而不写入内存,即使存储数据,读写操作也都能在高速缓存中进行而不用访问物理 RAM。无论在编译器一级或是硬件一级,指令的重新排序都有可能发生:一个指令序列如果以不同于程序文本中的次序运行常常能执行得更快,例如在防止 RISC 处理器流水线的互锁时就是如此。在 CISC 处理器上,耗时的操作则可以和运行较快的操作并发执行。
在对常规内存进行这些优化的时候,优化过程是透明的,而且效果良 好(至少在单处理器系统上是这样)。但对 I/O 操作来说这些优化很可能造成致命的错误,因为它们会干扰“边际效应”,而这却是驱动程序访问 I/O 寄存器的主要目的。处理器无法预料到某些其它进程(在另一个处理器上运行,或在某个 I/O 控制器中)是否会依赖于内存访问的顺序。因此驱动程序必须确保不会使用高速缓存,并且在访问寄存器时不会发生读或写指令的重新排序:编译器或 CPU 可能会自作聪明地重新排序所要求的操作,结果是发生奇怪的错误,并且很难调试。
由硬件自身缓存引起的问题很好解决:底层硬件配置成(可以是自动的或是由 Linux 初始化代码完成)访问 I/O 区域时(不管是内存还是端口)禁止硬件缓存就行了。
由编译器优化和硬件重新排序引起的问题的解决办法是,在从硬件角度看必须以特定顺序执行的操作之间设置内存屏障。Linux 提供了4个宏来解决所有可能的排序问题。
#include
void barrier(void)
这个函数通知编译器插入一个内存屏障,但对硬件无效。编译后的代码会把当前 CPU 寄存器中的所有修改过的数值存到内存,需要这些数据的时候再重新读出来。
#include
void rmb(void);
void wmb(void);
void mb(void);
这些函数在已编译的指令流中插入硬件内存屏障;具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb 保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier 的超集。
设备驱动程序中使用内存屏障的典型格式如下:
writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);
wmb();
writel(dev->registers.control, DEV_GO);
在这个例子中,最重要的是要确保控制某特定操作的所有设备寄存器一定要在操作开始之前正确设置。其中的内存屏障会强制写操作以必需的次序完成。
因为内存屏障会影响系统性能,所以应该只用于真正需要的地方。不 同类型的内存屏障影响性能的方面也不同,所以最好尽可能使用针对需要的特定类型。例如在当前的 x86 体系结构上,由于处理器之外的写不会重新排序,wmb 就没什么用。可是读会重新排序,所以 mb 就会比 wmb 慢一些。
注意其它大多数的处理同步的内核原语,如 spinlock 和 atomic_t 操作,也能作为内存屏障使用。
在有些体系结构上允许把赋值语句和内存屏障进行合并以提高效率。2.4 版本内核提供了几个执行这种合并的宏;它们默认情况下定义如下:
#define set_mb(var, value) do {var = value; mb();} while 0
#define set_wmb(var, value) do {var = value; wmb();} while 0
#define set_rmb(var, value) do {var = value; rmb();} while 0
在适当的地方,
头文件 sysdep.h 中定义了本节介绍的这些宏,可供缺少这些宏的平台和内核版本使用。
>>>>>IO寄存器与常规内存的不同,以及现代OS如何完成IO寄存器的读写<<<<<<
8.2 使用 I/O 端口
I/O 端口是驱动程序与许多设备的之间通信方式――至少在部分时间是这样。本节讲解了使用 I/O 端口的不同函数,另外也涉及到一些可移植性问题。
使用I/O 端口必须先分配,然后才能由驱动程序使用。用来分配和释放端口的函数是:
#include
int check_region(unsigned long start, unsigned long len);
struct resource *request_region(unsigned long start,
unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);
驱动程序请求了需要使用的 I/O
端口范围后,它必须读并且/或者写这些端口。为此,大多数硬件都把 8 位、16 位和 32
位的端口区分开来。它们不能象访问系统内存那样混淆*。因此,C
语言程序必须调用不同的函数来访问大小不同的端口。如前一节所述,那些只支持映射到内存的 I/O 寄存器的计算机体系结构通过把 I/O
端口地址重新映射到内存地址来模拟端口 I/O,并且为了易于移植,内核对驱动程序隐藏了这些细节。Linux
内核头文件中(就在与体系结构相关的头文件
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
按字节( 8 位宽度)读写端口。port 参数在一些平台上定义为 unsigned long,而在另一些平台上定义为 unsigned short。不同平台上 inb 返回值的类型也不相同。
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
这些函数用于访问 16 位端口(“字宽度”);不能用于 M68k 或 S390 平台,因为这些平台只支持字节宽度的 I/O 操作。
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
这些函数用于访问 32 位端口。longword 参数根据不同平台定义成 unsigned long 类型或 unsigned int 类型。和字宽度 I/O 一样,“长字”I/O 在 M68k 和 S390 平台上也不能用。
注意这里没有定义 64 位的 I/O 操作。即使在 64 位的体系结构上,端口地址空间也只使用最大 32 位的数据通路。
上面这些函数主要是提供给设备驱动程序使用的,但它们也可以在用户空间使用,至少在 PC 类计算机上可以使用。GNU 的 C 库在
8.2.1 串操作
以上的 I/O
操作都是一次传输一个数据,作为补充,有些处理器上实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字。这些指令称为串操作指
令,它们执行这些任务时比一个 C 语言写的循环语句快得多。下面列出的宏实现了串 I/O ,它们或者使用一条机器指令实现,或者在没有串 I/O
指令的平台上使用紧凑循环实现。M68k 和 S390
平台上没有定义这些宏。这不会影响可移植性,因为这些平台通常不会和其它平台使用同样的设备驱动程序,它们的外设总线不同。
串 I/O 函数的原型如下:
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
从内存地址 addr 开始连续读写 count 数目的字节。只对单一端口 port 读取或写入数据。
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
对一个16 位端口读写 16 位数据。
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
对一个 32 位端口读写 32 位数据。
8.2.2 暂停式 I/O
某些平台,特别是
i386 平台上,当处理器和总线之间的数据传输太快时会引起问题。因为相对于 ISA
总线,处理器的时钟频率太快,当设备板卡速度太慢时,这个问题就会暴露出来。解决方法是,如果一条I/O 指令后还跟着另一条 I/O
指令,就在两条指令间插入一小段延迟。如果有设备丢失数据的情况,或为了防止设备可能会丢失数据的情况,可以使用暂停式的 I/O 函数来取代通常的
I/O 函数。这些暂停式的 I/O 函数很象前面已经列出的那些 I/O 函数,不同之处是它们的名字用 _p 结尾;如
inb_p,outb_p,等等。在 Linux 支持的大多数平台上都定义了这些函数,不过它们常常扩展为和非暂停式 I/O
同样的代码,因为如果某种体系结构不使用过时的外设总线,就不需要额外的暂停。
8.2.3 平台相关性
由于自身的特性,I/O 指令是与处理器密切相关的。因为它们的工作涉及到处理器移入移出数据的细节,所以隐藏平台间的差异非常困难。因此,大部分与I/O端口有关的源代码都与平台相关。
I/O 操作在各个平台上执行的细节在对应平台的编程手册中有详细的叙述;也可从 Web 上下载这些手册的 PDF 文件。
>>>>>>IO操作的一些基本概念<<<<<<<<<
<<<<<<使用IO端口>>>>>>>>>>>>>>>>>>>>>>>>>>
数字 I/O 端口最普通的形式是一个字节宽度的 I/O 区域,它或者映射到内存,或者映射到端口。当数值写入到输出区域时,输出引脚上的电平信号随着写入的各位发生相应变化。从输入区域读到的数据则是输入引脚各位当前的逻辑电平值。
这类 I/O 端口的具体实现和软件接口是因系统而异的。大多数情况下,I/O 引脚是由两个 I/O 区域控制的:一个区域中可以选择用于输入和输出的引脚,另一个区域中可以读写实际逻辑电平。不过有时候情况简单些,每个位不是输入就是输出(不过在这种情 况下不能再称为“通用 I/O”了);所有个人计算机上都能找到的并口就是这样的非通用的 I/O 端口。
|
|