为了确保计算机能够正常工作,必须提供数据通路,让信息在连接到个人计算机的CPU、RAM和I/O设备之间流动。这些数据通路总称为总线,担当计算机内部主通信通道的作用。
所有计算机都拥有一条系统总线,它连接大部分内部硬件设备。一种典型的系统总线是PCI(Peripheral Component Interconnect)总线。目前使用其他类型的总线也很多,如ISA、EISA、MCA、SCSI和USB。
典型的情况是,一台计算机包括几种不同类型的总线,它们通过被称作“桥”的硬件设备连接在一起。两条高速总线用于在内存芯片上来回传送数据:前端总线将CPU连接到RAM控制器上,而后端总线将CPU直接连接到外部硬件的高速缓存上。主机上的桥将系统总线和前端总线连接在一起。
任何I/O设备有且仅能连接一条总线。总线的类型影响I/O设备的内部设计,也影响着内核如何处理设备。我们这篇博文将讨论所有PC体系结构共有的功能性特点,而不具体介绍特定总线类型的技术细节。
CPU和I/O设备之间的数据通路通常称为I/O总线。80x86微处理器使用16位的地址总线对I/O设备进行寻址,使用8位、16位或32位的数据总线传输数据。每个I/O设备依次连接到I/O总线上,这种连接使用了包含3个元素的硬件组织层次:I/O端口、接口和设备控制器。下图显示了I/O体系结构的这些成分:
每个连接到I/O总线上的设备都有自己的I/O地址集,通常称为I/O端口(I/O port)。在IBM PC体系结构中,I/O地址空间一共提供了65536个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端口都被组织成如下图所示的一组专用寄存器。CPU把要发送给设备的命令写入设备控制寄存器(device control register),并从设备状态寄存器(device status register)中读出表示设备内部状态的值。CPU还可以通过读取设备输入寄存器(device input register)的内容从设备取得数据,也可以通过向设备输出寄存器(device output register)中写入字节而把数据输出到设备。
为了降低成本,通常把同一I/O端口用于不同目的。例如,某些位描述设备的状态,而其他位指定向设备发出的命令。同理,也可以把同一I/O端口用作输入寄存器或输出寄存器。
in、out、ins和outs汇编语言指令都可以访问I/O端口。内核中包含了以下辅助函数来简化这种访问:
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个字节为一组的连续字节序列。
80x86没有使用以上这些辅助函数,因为对于直接使用in、out、ins和outs这些汇编语言是再好不过了,如果你要分析arm、mpis等其他体系结构,可以看看他们。我们主要分析x86体系,所以就不去管他们了。
虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单了。通常,I/O设备驱动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用了这个端口,那么系统就会崩溃。为了防止这种情况的发生,内核必须使用“资源”来记录分配给每个硬件设备的I/O端口。
资源(resource)表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在我们的情况中,一个资源表示I/O端口地址的一个范围。每个资源对应的信息存放在resource数据结构中。所有的同种资源都插入到一个树型数据结构中;例如,表示I/O端口地址范围的所有资源都包含在一个根节点为ioport_resource的树中:
struct resource {
resource_size_t start; /* 资源范围的开始 */
resource_size_t end; /* 资源范围的结束 */
const char *name; /* 资源拥有者的描述 */
unsigned long flags; /* 各种标志 */
/* 指向资源树中父亲、兄弟和第一个孩子的指针 */
struct resource *parent, *sibling, *child;
};
struct resource ioport_resource = {
.name = "PCI IO",
.start = 0,
.end = IO_SPACE_LIMIT,
.flags = IORESOURCE_IO,
};
#define IO_SPACE_LIMIT 0xffff /* x86体系是16位总线端口地址 */
注意,第一个I/O端口资源赋给了PCI接口。节点的孩子被收集在一个链表中,其第一个元素由child指向。sibling字段指向链表中的下一个节点。
那么,为什么要使用树,而不是其他诸如链表等形式呢?例如,考虑一下SCSI磁盘接口所使用的I/O端口地址 —— 比如说从0xf000到0xf00f。然后,start字段为0xf000且end字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,SCSI设备驱动程序需要记住另外的信息,也就是SCSI链(SCSI chain)的主盘(master disk)使用0xf000~0xf007的子范围,从盘(slave disk)使用0xf008~0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到0xf00O~0xf00f的整个范围对应的资源下。一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树(ioport_resource)的根节点跨越了整个I/O地址空间(从端口0~65535)。一个典型的PC I/O端口资源分配如下:
0000~000F:DMA控制器1
0020~0021:主中断控制器
0040~0043:系统时钟
0060:键盘控制器控制状态口
0061:系统扬声器
0064:键盘控制器数据口
0070~0071:系统CMOS/实时钟
0080~0083:DMA控制器1
0087~0088:DMA控制器1
0089~008B:DMA控制器1
00A0~00A1:从中断控制器
00C0~00DF:DMA控制器2
00F0~00FF:数值协处理器
0170~0117:标准IDE/ESDI硬盘控制器
01F0~01FF:标准IDE/ESDI硬盘控制器
0200~0207:游戏口
0274~0277:ISA即插即用计数器
0278~027F:并行打印机口
02F8~02FF:串行通信口2(COM2)
0376:第二个IDE硬盘控制器
0378~037F:并行打印口1
03B0~03BB:VGA显示适配器
03C0~03DF:VGA显示适配器
03D0~03DF:彩色显示器适配器
03F2~03F5:软磁盘控制器
03F6:第一个硬盘控制器
03F8~03FF:串行通信口1(COM1)
0400~FFFF没有指明端口,供用户扩展使用
任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:
request_resource() —— 把一个给定范围分配给一个I/O设备。
int request_resource(struct resource *root, struct resource *new)
{
struct resource *conflict;
write_lock(&resource_lock);
conflict = __request_resource(root, new);
write_unlock(&resource_lock);
return conflict ? -EBUSY : 0;
}
static struct resource * __request_resource(struct resource *root, struct resource *new)
{
resource_size_t start = new->start;
resource_size_t end = new->end;
struct resource *tmp, **p;
if (end < start)
return root;
if (start < root->start)
return root;
if (end > root->end)
return root;
p = &root->child;
for (;;) {
tmp = *p;
if (!tmp || tmp->start > end) {
new->sibling = tmp;
*p = new;
new->parent = root;
return NULL;
}
p = &tmp->sibling;
if (tmp->end < start)
continue;
return tmp;
}
}
allocate_resource() —— 在资源树中寻找一个给定大小和排列方式的可用范围;若存在,就将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,这种驱动程序可以配置成使用任意的端口号和主板上的内存地址对其进行配置)。
int allocate_resource(struct resource *root, struct resource *new,
resource_size_t size, resource_size_t min,
resource_size_t max, resource_size_t align,
void (*alignf)(void *, struct resource *,
resource_size_t, resource_size_t),
void *alignf_data)
{
int err;
write_lock(&resource_lock);
err = find_resource(root, new, size, min, max, align, alignf, alignf_data);
if (err >= 0 && __request_resource(root, new))
err = -EBUSY;
write_unlock(&resource_lock);
return err;
}
static int find_resource(struct resource *root, struct resource *new,
resource_size_t size, resource_size_t min,
resource_size_t max, resource_size_t align,
void (*alignf)(void *, struct resource *,
resource_size_t, resource_size_t),
void *alignf_data)
{
struct resource *this = root->child;
new->start = root->start;
/*
* Skip past an allocated resource that starts at 0, since the assignment
* of this->start - 1 to new->end below would cause an underflow.
*/
if (this && this->start == 0) {
new->start = this->end + 1;
this = this->sibling;
}
for(;;) {
if (this)
new->end = this->start - 1;
else
new->end = root->end;
if (new->start < min)
new->start = min;
if (new->end > max)
new->end = max;
new->start = ALIGN(new->start, align);
if (alignf)
alignf(alignf_data, new, size, align);
if (new->start < new->end && new->end - new->start >= size - 1) {
new->end = new->start + size - 1;
return 0;
}
if (!this)
break;
new->start = this->end + 1;
this = this->sibling;
}
return -EBUSY;
}
release_resource() —— 释放以前分配给I/O设备的给定范围。
int release_resource(struct resource *old)
{
int retval;
write_lock(&resource_lock);
retval = __release_resource(old);
write_unlock(&resource_lock);
return retval;
}
static int __release_resource(struct resource *old)
{
struct resource *tmp, **p;
p = &old->parent->child;
for (;;) {
tmp = *p;
if (!tmp)
break;
if (tmp == old) {
*p = tmp->sibling;
old->parent = NULL;
return 0;
}
p = &tmp->sibling;
}
return -EINVAL;
}
内核也为以上应用于I/O端口的函数定义了一些快捷函数:request_region()分配I/O端口的给定范围,release_region()释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。下面给大家展示一下本人的系统环境的I/O端口的最终分配情况:
[root@localhost proc]# cat ioports
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-0060 : keyboard
0064-0064 : keyboard
0070-0077 : rtc
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
02f8-02ff : serial
03c0-03df : vga+
03f8-03ff : serial
0400-041f : 0000:00:1f.3
0400-041f : i801_smbus
0800-0803 : ACPI PM1a_EVT_BLK
0804-0805 : ACPI PM1a_CNT_BLK
0808-080b : ACPI PM_TMR
0810-0815 : ACPI CPU throttle
0820-082f : ACPI GPE0_BLK
0850-0850 : ACPI PM2_CNT_BLK
0a00-0a0f : pnp 00:08
0ca2-0ca3 : pnp 00:0c
0ca2-0ca2 : ipmi_si
0ca3-0ca3 : ipmi_si
a400-a41f : 0000:00:1d.0
a400-a41f : uhci_hcd
a480-a49f : 0000:00:1d.1
a480-a49f : uhci_hcd
a800-a81f : 0000:00:1d.2
a800-a81f : uhci_hcd
a880-a89f : 0000:00:1a.0
a880-a89f : uhci_hcd
ac00-ac1f : 0000:00:1a.1
ac00-ac1f : uhci_hcd
b000-b01f : 0000:00:1a.2
b000-b01f : uhci_hcd
b400-b407 : 0000:00:1f.2
b480-b49f : 0000:00:1f.2
b800-b803 : 0000:00:1f.2
b880-b887 : 0000:00:1f.2
bc00-bc03 : 0000:00:1f.2
c000-cfff : PCI Bus #01
cc00-cc1f : 0000:01:00.0
d000-dfff : PCI Bus #02
dc00-dc1f : 0000:02:00.0
e000-efff : PCI Bus #05
e400-e4ff : 0000:05:00.0
e800-e8ff : 0000:05:00.1
注意咯,其实这个树的最大深度也就是2。因为主从关系最多也就从到2。
I/O接口(I/O interface)是处于一组I/O端口和对应的设备控制器之间的一种硬件电路。它起翻译器的作用,即把I/O端口中的值转换成设备所需要的命令和数据。在相反的方向上,它检测设备状态的变化,并对起状态寄存器作用的I/O端口进行相应的更新。还可以通过一条IRQ线把这种电路连接到可编程中断控制器上,以使它代表相应的设备发出中断请求。
有两种类型的接口:
专门用于一个特定的硬件设备。在一些情况下,设备控制器与这种I/O接口处于同一块卡中(每块卡都要擂入PC的一个可用空闲总线插槽中。如果一块卡通过一条外部电缆连接到一个外部设备上,那么在PC后面的面板中就有一个对应的连接器。)。连接到专用I/O接口上的设备可以是内部设备(位于PC机箱内部的设备),也可以是外部设备(位于PC机箱外部的设备)。
专用I/O接口的种类很多,因此目前已装在PC上设备的种类也很多,我们无法一一列出,在此只列出一些最通用的接口:
键盘接口:
连接到一个键盘控制器上,这个控制器包含一个专用微处理器。这个微处理器对按下的组合键进行译码,产生一个中断并把相应的键盘扫描码写人输入寄存器。
图形接口:
和图形卡中对应的控制器封装在一起,图形卡有自己的帧缓冲区,还有一个专用处理器以及存放在只读存储器(ROM)芯片中的一些代码。帧缓冲区是显卡上固化的存储器,其中存放的是当前屏幕内容的图形描述。
磁盘接口:
由一条电缆连接到磁盘控制器,通常磁盘控制器与磁盘放在一起。例如,IDE接口由一条40线的带形电缆连接到智能磁盘控制器上,在磁盘本身就可以找到这个控制器。
总线鼠标接口:
由一条电缆把接口和控制器连接在一起,控制器就包含在鼠标中。
网络接口:
与网卡中的相应控制器封装在一起,用以接收或发送网络报文。虽然广泛采用的网络标准很多,但还是以太网(IEEE 802.3)最为通用。
用来连接多个不同的硬件设备。连接到通用I/O接口上的设备通常都是外部设备。现代PC都包含连接很多外部设备的几个通用I/O接口。最常用的接口有:
并口:
传统上用于连接打印机,它还可以用来连接可移动磁盘、扫描仪、备份设备、其他计算机等等。数据的传送以每次1字节(8位)为单位进行。
串口:
与并口类似,但数据的传送是逐位进行的。串口包括一个通用异步收发器(UART)芯片,它可以把要发送的字节信息拆分成位序列,也可以把接收到的位流重新组装成字节信息。由于串口本质上速度低于并口,因此主要用于连接那些不需要高速操作的外部设备,如调制解调器、鼠标以及打印机。
PCMCIA接口:
大多数便携式计算机都包含这种接口。在不重新启动系统的情况下,这种形状类似于信用卡的外部设备可以被插入插槽或从插槽中拔走。最常用的PCMCIA设备是硬盘、调制解调器、网卡和扩展RAM。
SCSI(小型计算机系统接口)接口:
是把PC主总线连接到次总线(称为SCSI总线)的电路。SCSI-2总线允许一共8个PC和外部设备(硬盘、扫描仪、CR-ROM刻录机等等)连接在一起。如果有附加接口,宽带SCSI-2和新的SCSI-3接口可以允许你连接多达16个以上的设备。SCSI标准是通过SCSI总线连接设备的通信协议。
通用串行总线(USB):
高速运转的通用I/O接口,可用于连接外部设备,代替传统的并口、串口以及SCSI接口。
3 设备控制器
复杂的设备可能需要一个设备控制器(device controller)来驱动。从本质上说,控制器起两个重要作用:
(1)对从I/O接口接收到的高级命令进行解释,并通过向设备发送适当的电信号序列强制设备执行特定的操作。
(2)对从设备接收到的电信号进行转换和适当地解释,并修改(通过I/O接口)状态寄存器的值。
典型的设备控制器是磁盘控制器,它从微处理器(通过I/O接口)接收诸如“写这个数据块”之类的高级命令,并将其转换成诸如“把磁头定位在正确的磁道上”和“把数据写入这个磁道”之类的低级磁盘操作。现代的磁盘控制器相当复杂,因为它们可以把磁盘数据快速保存到内存的高速缓存中,还可以根据实际磁盘的几何结构重新安排CPU的高级请求,使其最优化。
比较简单的设备没有设备控制器,可编程中断控制器和可编程间隔定时器就是这样的设备。
很多硬件设备都有自己的存储器,通常称之为I/O共享存储器。例如,所有比较新的图形卡在帧缓冲区中都有几MB的RAM,用它来存放要在屏幕上显示的屏幕映像。