Chinaunix首页 | 论坛 | 博客
  • 博客访问: 370502
  • 博文数量: 94
  • 博客积分: 3421
  • 博客等级: 中校
  • 技术积分: 919
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-18 16:27
文章分类

全部博文(94)

文章存档

2015年(8)

2013年(6)

2012年(26)

2011年(8)

2010年(26)

2009年(20)

我的朋友

分类: 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 内核头文件中(就在与体系结构相关的头文件 中)定义了如下一些访问 I/O 端口的内联函数。

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 库在 中定义了这些函数。如果要在用户空间代码中使用 inb 及其相关函数,必须满足某些条件。

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 端口。



看看这个~~~~

LINUX 硬件管理 嵌入式 Linux-uClinux 内核技术
第 8 章 硬件管理


尽管摆弄 scull 和其他一些玩具程序对理解 Linux 设备驱动程序的软件接口很有帮助,但实现真正的设备还是要涉及实际硬件。设备驱动程序是软件概念和硬件电路之间的一个抽象层,因此,两者都需要谈谈。到现 在为止,我们已经详细讨论了软件上的一些细节;本章将完成另外一部分,介绍驱动程序是如何在保持可移植性的前提下访问 I/O 端口和 I/O 内存的。

和前面一样,本章尽可能不针对特定设备。在需要示例的场合,我们使用简单的数字 I/O 端口(比如标准 PC 并口)来讲解 I/O 指令,使用普通的帧缓存显示内存来讲解内存映射 I/O。

我们选择使用并口是因为它是最简单的输入/输出端口。几乎所有的 计算机上都有并口,并实现了原始的 I/O:写到设备的数据位出现在输出引脚上,而输入引脚的电压值可以由处理器直接获取。实践中,我们必须将 LED 连接到并口上才能真正“看到”数字 I/O 操作的结果,相关底层硬件也非常容易使用。

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 核心访问内存更有效率,访问内存时,编译器在寄存器分配和寻址方式选择上也有更多的自由。

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 中定义了本节介绍的这些宏,可供缺少这些宏的平台和内核版本使用。

8.2 使用 I/O 端口
I/O 端口是驱动程序与许多设备的之间通信方式――至少在部分时间是这样。本节讲解了使用 I/O 端口的不同函数,另外也涉及到一些可移植性问题。

我们先回忆一下,I/O 端口必须先分配,然后才能由驱动程序使用。这在第 2 章的“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 内核头文件中(就在与体系结构相关的头文件 中)定义了如下一些访问 I/O 端口的内联函数。

注:有时 I/O 端口是和内存一样对待的,(例如)可以将 2 个 8 位的操作合并成一个 16 位的操作。例如,PC 的显示卡就可以,但一般来说不能认为一定具有这种特性。

警告:从现在开始,如果我使用 unsigned 而不进一步指定类型信息的话,那么就是在谈及一个与体系结构相关的定义,此时不必关心它的准确特性。这些函数基本是可移植的,因为编译器在赋值时会自动进 行强制类型转换 (cast)--强制转换成 unsigned 类型防止了编译时出现的警告信息。只要程序员赋值时注意避免溢出,这种强制类型转换就不会丢失信息。在本章剩余部分将会一直保持这种“不完整的类型定义” 的方式。

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 库在 中定义了这些函数。如果要在用户空间代码中使用 inb 及其相关函数,必须满足下面这些条件:


编译该程序时必须带 -O 选项来强制内联函数的展开。

必须用 ioperm 或 iopl 来获取对端口进行I/O操作的许可。ioperm 用来获取对单个端口的操作许可,而 iopl 用来获取对整个 I/O 空间的操作许可。这两个函数都是 Intel 平台特有的。

必须以 root 身份运行该程序才能调用 ioperm 或 iopl*。或者,该程序的某个祖先已经以 root 身份获取了对端口操作的权限。

注:从技术上说,必须有 CAP_SYS_RAWIO 的权能,不过这与在当前系统以 root 身份运行是一样的。

如果宿主平台没有 ioperm 和 iopl 系统调用,用户空间程序仍然可以使用 /dev/port 设备文件访问 I/O 端口。不过要注意,该设备文件的含义和平台的相关性是很强的,并且除 PC 上以外,它几乎没有用处。

示例程序 misc-progs/inp.c 和 misc-progs/outp.c 是在用户空间通过命令行读写端口的一个小工具。它们会以多个名字安装(如 inpb、inpw,inpl)并且按用户调用的名字分别操作字节端口、字端口或双字端口。如果没有 ioperm,它们就使用 /dev/port。

如果想冒险,可以将它们设置上 SUID 位,这样,不用显式地获取特权就可以使用硬件了。

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端口有关的源代码都与平台相关。

回头看看前面的函数列表,可以看到一处不兼容的地方:数据类型。 函数的参数类型根据各平台体系结构上的不同要相应地使用不同的数据类型。例如,port 参数在 x86 平台(处理器只支持 64KB 字节的 I/O 空间)上定义为 unsigned short,但在其它平台上定义为 unsigned long。在那些平台上端口是与内存在同一地址空间内的一些特定区域。

其他一些与平台相关的问题来源于处理器基本结构上的差异,因此也无法避免。因为本书假定读者不会在不了解底层硬件的情况下为特定的系统编写驱动程序,所以不会详细讨论这些差异。下面是内核 2.4 版本支持的体系结构可以使用的函数的总结:

IA-32 (x86)
该体系结构支持本章提到的所有函数。端口号的类型是 unsigned short。

IA-64 (Itanium)
支持所有函数;端口类型是unsigned long(映射到内存)。串操作函数是用 C 语言实现的。

Alpha
支持所有函数,而 I/O 端口是映射到内存的。基于不同的 Alpha 平台上使用的芯片组的不同,端口 I/O 操作的实现也有所不同。串操作是用 C 语言实现的,在文件 arch/alpha/lib/io.c 中定义。端口类型是 unsigned long。

ARM
端口映射到内存,支持所有函数;串操作用 C 语言实现。端口类型是 unsigned int。

M68k
端口映射到内存,只支持字节类型的函数。不支持串操作,端口类型是 unsigned char *。

MIPS
MIPS64
MIPS 端口支持所有函数。因为该处理器不提供机器一级的串 I/O 操作,所以串操作是用汇编语言写的紧凑循环(tight loop)实现的。端口映射到内存;端口类型在 32 位处理器上是 unsigned int,在 64 位处理器上是 unsigned long。

PowerPC
支持所有函数;端口类型为 unsigned char *。

S390
类似于 M68k,该平台的头文件只支持字节宽度的端口 I/O,不支持串操作。端口类型是字符型(char)指针,映射到内存。

Super-H
端口类型是 unsigned int(映射到内存),支持所有函数。

SPARC
SPARC64
和前面一样,I/O 空间映射到内存。端口操作函数的 port 参数类型是 unsigned long。

感兴趣的读者可以从 io.h 文件获得更多信息,除了在本章介绍的函数,一些与体系结构相关的函数有时也由该文件定义。不过要注意这些文件阅读起来会比较困难。

值得提及的是,x86 家族之外的处理器都不为端口提供不同的地址空间,尽管使用其中几种处理器的机器带有 ISA 和 PCI 插槽(两种总线都实现了不同的 I/O 和内存地址空间)。

除此以外,一些处理器(特别是早期的 Alpha 处理器)没有一次传输 1 或 2 个字节的指令*。因此,它们的外设芯片通过把端口映射到内存地址空间的特殊地址范围来模拟 8 位和 16 位的 I/O 访问。这样,对同一个端口的 inb 和 inw 指令实现为两个 32 位的读不同内存地址的操作。幸好,本章前面介绍的宏的内部实现对驱动程序开发人员隐藏了这些细节,不过这个特点还是很有趣的。想进一步深入的读者可以看 include/asm-alpha/core_lca.h 中的例子。

注:单字节 I/O 操作并没有想象中那么重要,因为这种操作很少发生。为了读写任意地址空间的单个字节,需要实现一条从寄存器组数据总线低位到外部数据总线任意字节地址的数 据通路。这种数据通路在每一次数据传输中都需要额外的逻辑门。不使用这类字节宽度的存取指令可以提升系统总体性能。

I/O 操作在各个平台上执行的细节在对应平台的编程手册中有详细的叙述;也可从 Web 上下载这些手册的 PDF 文件。

8.3 使用数字 I/O 端口
我们用来演示设备驱动程序的端口 I/O 的示例代码工作于通用的数字 I/O 端口上;这种端口在大多数计算机平台上都能找到。

数字 I/O 端口最普通的形式是一个字节宽度的 I/O 区域,它或者映射到内存,或者映射到端口。当数值写入到输出区域时,输出引脚上的电平信号随着写入的各位发生相应变化。从输入区域读到的数据则是输入引脚各位当前的逻辑电平值。

这类 I/O 端口的具体实现和软件接口是因系统而异的。大多数情况下,I/O 引脚是由两个 I/O 区域控制的:一个区域中可以选择用于输入和输出的引脚,另一个区域中可以读写实际逻辑电平。不过有时候情况简单些,每个位不是输入就是输出(不过在这种情 况下不能再称为“通用 I/O”了);所有个人计算机上都能找到的并口就是这样的非通用的 I/O 端口。我们随后介绍的示例代码要用到这些 I/O 引脚。

8.4 使用 I/O 内存
除了 x86 上普遍使用的 I/O 端口,和设备通信的另一种主要机制是通过使用映射到内存的寄存器或设备内存。这两种都称为 I/O 内存,因为寄存器和内存的差别对软件是透明的。(<<<寄存器可以映射到内存,IO内存也可以映射到内存)

I/O 内存仅仅是类似 RAM 的一个区域,在那里处理器可以通过总线访问设备。这种内存有很多用途,比如存放视频数据或网络包;这些用设备寄存器也能实现,其行为类似于 I/O 端口(比如,读写时有边际效应)。

访问 I/O 内存的方法和计算机体系结构、总线,以及设备是否正在使用有关,不过原理都是相同的。本章主要讨论 ISA 和 PCI 内存,同时也试着介绍一些通用的知识。尽管这里介绍了 PCI 内存的访问,但关于 PCI 的详细讨论将放到第 15 章中进行。

根据计算机平台和所使用总线的不同,I/O 内存可能是,也可能不是通过页表访问的。如果访问是经由页表进行的,内核必须首先安排物理地址使其对设备驱动程序可见(这通常意味着在进行任何 I/O 之前必须先调用 ioremap)。如果访问无需页表,那么 I/O 内存区域就很象 I/O 端口,可以使用适当形式的函数读写它们。《《《IOREMAP《《《

不管访问 I/O 内存时是否需要调用 ioremap,都不鼓励直接使用指向 I/O 内存的指针。尽管(在“I/O 端口和 I/O 内存”介绍过)I/O 内存在硬件一级是象普通 RAM 一样寻址的,但在“I/O 寄存器和常规内存”中描述过的那些需要额外小心的情况中已经建议不要使用普通指针。相反,使用“包装的”函数访问 I/O 内存,一方面在所有平台上都是安全的,另一方面,在可以直接对指针指向的内存区域执行操作的时候,该函数是经过优化的。

设备内存区域在使用前必须先分配。这和 I/O 端口注册过程类似,是由下列函数完成的:
int check_mem_region(unsigned long start, unsigned long len);
void request_mem_region(unsigned long start, unsigned long len,
char *name);
void release_mem_region(unsigned long start, unsigned long len);
传给函数的 start 参数是内存区的物理地址,此时还没有发生任何重映射


阅读(5359) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~