每个连接到I/O总线上的设备都有自己的I/O地址集,即所谓的I/O端口(I/O
port)。在IBM PC体系结构中,I/O地址空间一共提供了65,536个8位的I/O端口。可以把两个连续的8位端口看成一个16位端口,但是这必须是从偶数地址开始。同理,也可以把两个连续的16位端口看成一个32位端口,但是这必须是从4的整数倍地址开始。有四条专用的汇编语言指令可以允许CPU对I/O端口进行读写:它们分别是in、ins、out和outs。在执行其中的一条指令时,CPU使用地址总线选择所请求的I/O端口,使用数据总线在CPU寄存器和端口之间传送数据。
I/O端口还可以被映射到物理地址空间:因此,处理器和I/O设备之间的通信就可以直接使用对内存进行操作的汇编语言指令(例如,mov、and、or等等)。现代的硬件设备更倾向于映射I/O,因为这样处理的速度较快,并可以和DMA结合起来使用。
系统设计者的主要目的是提供对I/O编程的统一方法,但又不牺牲性能。为了达到这个目的,每个设备的I/O 端口都被组织成如图11.4所示的一组专用寄存器。CPU把要发给设备的命令写入控制寄存器(control register),并从状态寄存器(status register)中读出表示设备内部状态的值。CPU还可以通过读取输入寄存器(input register)的内容从设备取得数据,也可以通过向输出寄存器(output
register)中写入字节而把数据输出到设备。
图11.4 专用I/O端口
为了降低成本,通常把同一I/O端口用于不同目的。例如,某些位描述设备的状态,而其他位指定发布给设备的命令。同理,也可以把同一I/O端口用作输入寄存器或输出寄存器。
那么如何访问I/O端口? in、out、ins和outs汇编语言指令都可以访问I/O端口。Linux内核中定义了以下辅助函数来简化这种访问:
· inb(
)、inw( )、inl( )函数
分别从I/O端口读取1、2或4个连续字节。后缀“b”、“w”、“l”分别代表一个字
节(8位)、一个字(16位)以及一个长整型(32位)。
· inb_p(
)、inw_p( )、inl_p( )
分别从I/O端口读取1、2或4个连续字节,然后执行一条“哑元(dummy,即空指令)”指令使CPU暂停。
· outb(
)、outw( )、outl( )
分别向一个I/O端口写入1、2或4个连续字节。
· outb_p(
)、outw_p( )、outl_p( )
分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“哑元”指令使CPU暂停。
· insb(
)、insw( )、insl( )
分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。
· outsb(
)、outsw( )、outsl( )
分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。
虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单,特别是对基于ISA总线的系统来说更是如此。通常,I/O设备驱动程序为了侦探硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内核必须使用iotable表来记录分配给每个硬件设备的I/O端口。任何设备驱动程序都可以使用下面三个函数:
request_region( )
把一个给定区间的I/O端口分配给一个I/O设备。
check_region( )
检查一个给定区间的I/O端口是否空闲,或者其中一些是否已经分配给某个I/O设备。
release_region( )
释放以前分配给一个I/O设备的给定区间的I/O端口。
当前分配给I/O设备的I/O地址可以从/proc/ioports文件中获得。