分类:
2012-09-07 15:46:45
原文地址:Linux内核中IO端口资源管理 作者:Dejun.Liu
计算机通过读写设备上的寄存器来控制设备的。这些外设寄存器又称为"I/O端口",它们通常分为控制寄存器、状态寄存器和数据寄存器三大类,而且外设的寄存器通常被连续地编址。CPU对外设I/O端口物理地址的编址方式有两种:一种是I/O端口地址方式,另一种是存储器地址方式。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,这可以通过系统固件(如BIOS)在启动时分配得到,或者通过设备的硬连线(hardwired)得到。比如,PCI卡的I/O内存资源的物理地址就是在系统启动时由PCI BIOS分配并写到PCI卡的配置空间中的。而ISA卡的I/O内存资源的物理地址则是通过设备硬连线映射到640KB-1MB范围之内的。
这章介绍了资源的管理机制。
目录 [] |
Linux内核使用二叉树管理资源,通常二对树只有两层。它提供了通用的API函数管理资源二叉树,不同类型的资源都调用该API完成资源的管理。
Linux内核将某一类的资源物理内存空间用根节点描述,不同的资源节点占用这个空间的一部分,这些资源节点作为根节点的孩子节点链接成孩子链表。
例如:在图4所示的资源中,根资源A的物理内存范围为0~0xef10,它含有三个子资源节点B、C、D,分别占有这个物理空间的一部分,这三个子节点之间存在着内存空洞。
Linux设计了一个通用的数据结构resource(又称为资源结构或资源描述子)来描述各种I/O资源(如:I/O端口、外设内存、DMA和IRQ等)。利用二叉树链表将不同类型的I/O资源分别连接成二叉树,二叉树的层次关系也反映了资源的层次关系。资源管理的数据结构(include/linux/ioport.h)如下:
PCI总线的资源还用链表结构resource_list链接成链表,结构resource_list列出如下:
例如,I/O端口资源ioport_resource定义如下(在kernel/resource.c中):
Linux用树形结构来管理每一类I/O资源(如:I/O端口、外设内存、DMA和IRQ)。每一类I/O资源都对应一个资源树,树中的每一个节点都是一个resource结构。
函数request_resource申请并保留一个I/O或内存资源。参数root为根资源描述子,参数new为调用者期望得到的资源描述子。如果申请成功,函数返回0,否则,返回错误代码。
函数request_resource列出如下(在kernel/resource.c中):
函数__request_resource完成资源分配工作。如果参数new所描述的资源中的一部分或全部已经被其它节点所占用,则函数返回与new相冲突的资源root。否则,再遍历root的孩子,根据资源值范围将new插入到root的孩子链表就返回NULL。
函数__request_resource列出如下(在kernel/Resource.c中):
函数release_resource释放以前保留的资源,参数old为旧资源的指针。其列出如下(在kernel/resource.c中):
函数__release_resource遍历孩子链表,如果找到资源节点,就从链表中删除。其列出如下:
函数allocate_resource在资源树的孩子节点之间查找适合指定条件的物理内存空洞给new,分配的区域还应字节对齐。然后,它再申请资源new,查看new是否与资源树中的资源相冲突,如果没有冲突,就将new加入到资源树中。
函数allocate_resource的参数说明如下:
root: 根资源描述子。
new: 调用者期望分配的资源描述子。
size: 请求的资源区域大小。
min: 需要分配的最小尺寸。
max: 需要分配的最大尺寸。
align: 用来对齐请求区域的字节数。
alignf: 可选的对齐函数。
alignf_data: 传递给对齐函数的数据。
函数allocate_resource列出如下(在kernel/resource.c中):
函数find_resource从孩子链表的孩子之间找到适合给定条件的资源空洞,赋给new。该函数列出如下:
不同的CPU构架,I/O端口可能基于I/O端口地址方式或者基于存储器地址方式,Linux将这两种方式的I/O端口资源统一称作为"I/O区域(I/O Region)"。I/O区域资源用结构resource描述。
I/O端口有两种地址方式,中断和DMA是两种较特殊的端口资源,Linux内核将它们单独定义出来,其实,它们也只是两种地址方式之一。I/O端口的类型定义如下(在include/linux/ioport.h中):
为了方便操作,Linux内核为两种地址方式的端口分别提供了操作宏,它们定义如下(在include/linux/ioport.h中):
从上面的宏定义可以看出,Linux内核为两种地址方式的I/O区域提供了统一的I/O区域操作函数,I/O区域操作函数又通过资源管理函数API完成具体的资源管理的。
两种地址方式的I/O区域的资源树根节点分别列出如下(在kernel/resource.c中):
从上述资源定义可以看出,I/O端口地址方式的I/O地址空间很小,一般小于64K,而存储器地址方式的I/O地址可以设计得很大。它们的起始地址为0。
Linux内核还将不同类型的虚拟地址空间作为资源管理,定义为存储器地址方式的资源。
函数__request_region先分配资源结构实例,设置需要分配资源的地址;再遍历资源树,调用函数__request_resource检查分配资源的地址范围是否与树中节点资源有冲突。如果资源地址范围与树中节点不冲突,说明分配成功。同时,函数__request_resource还会将新分配的资源加入到资源树中。
函数__request_resource只检查节点及它的孩子链表,遍历资源树,需要通过不断降低树节点的层次,循环调用函数__request_resource,只到遍历完整个树节点。每次循环时, 如果函数__request_resource()检查到资源冲突并返回冲突的资源时,则进一步判断所返回的冲突资源节点是否就是父资源节点parent。如果不是,则下降一个层次,即试图在当前冲突的资源节点中进行分配,只有在冲突的资源节点没有设置IORESOURCE_BUSY的情况下才可以)。
函数__request_region的参数parent为父资源描述子,参数start为分配资源的起始地址,参数n为分配资源区域的大小,参数name为资源名,是保留给调用者的ID字符串。该函数列出如下(在kernel/resource.c中):
函数__check_region()检查给定的I/O区域是否被被占用。该函数列出如下:
函数__release_region释放以前保留的资源区域,并从资源树中删除资源区域所对应的节点。参数parent为父资源描述子,参数start为资源起始地址,参数n为资源区域大小。
函数__release_region遍历资源树,如果释放的地址区域在当前节点res的地址范围之内,且节点资源不忙,就将孩子节点设为当前节点,继续遍历树的下一层。否则,继续遍历孩子链表。找到指定区域的资源节点后,从树中删除该节点。
函数__release_region列出如下:
Linux将I/O区域信息显示在/proc/iomem和/proc/ioports文件中,它们分别对应存储器地址区域和I/O端口地址区域,这两个文件内容列出如下:
文件/proc/iomem显示了存储器地址区域,它是各种虚拟地址空间的分配,包括设备物理地址空间在虚拟地址空间的映射、用户空间、内核代码与数据空间、保留区域等。保留区域直到隔离带保护作用。
对于x86构架,文件/proc/ioports显示了I/O端口物理地址空间(一般为64K),它独立于内存空间,Linux内核通过专门的I/O端口访问指令访问这段物理地址空间。
系统上物理设备的端口空间按存储器地址方式或I/O端口地址方式进行访问。对于x86构架来说,CPU提供端口控制的设备,采用I/O端口地址方式,其他外围设备,采用存储器地址方式。
高级可编程中断控制器(Advanced Programmable Interrupt Controller)
驱动程序不能直接访问物理内存,为了让软件可以访问 I/O 内存,必须使用ioremap 函数把虚拟地址赋于设备。ioremap 函数把虚拟地址指定到 I/O 内存区域。
Linux在include/asm-i386/io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
void iounmap(void * addr);
函数用于取消ioremap()所做的映射,参数addr是指向核心虚地址的指针。这两个函数都是实现在mm/ioremap.c文件中。
设备驱动程序通过ioremap 和 iounmap就能访问任何 I/O 内存地址,而不管它是否直接映射到虚拟地址空间。这些地址不能直接引用,而应该使用象 readb 这样的函数。
由于在某些平台上,对I/O内存和系统内存有不同的访问处理,因此为了确保跨平台的兼容性,Linux实现了一系列读写I/O内存资源的函数,这些函数在不同的平台上有不同的实现。但在x86平台上,读写I/O内存与读写RAM无任何差别。如下所示(include/asm-i386/io.h):
上述定义中的宏__io_virt()仅仅检查虚地址addr是否是核心空间中的虚地址。具体的实现函数在arch/i386/lib/iodebug.c文件。
显然,在x86平台上访问I/O内存资源与访问系统主存RAM是无差别的。但是为了保证驱动程序的跨平台的可移植性,我们应该使用上面的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。
大多数平台都区分8位、16位和32位宽度的I/O端口的。Linux在include/asm/io.h头文件(对于i386平台就是include/asm-i386/io.h)中定义了一系列读写不同宽度I/O端口的宏函数。如下所示:
(1)读写8位宽的I/O端口
unsigned char inb(unsigned port);
void outb(unsigned char value,unsigned port);
其中,port参数指定I/O端口空间中的端口地址。在大多数平台上(如x86)它都是unsigned short类型的,其它的一些平台上则是unsigned int类型的。显然,端口地址的类型是由I/O端口空间的大小来决定的。
(2)读写16位宽的I/O端口
unsigned short inw(unsigned port);
void outw(unsigned short value,unsigned port);
(3)读写32位宽的I/O端口
unsigned int inl(unsigned port);
void outl(unsigned int value,unsigned port);
Linux同样在io.h文件中定义了字符串I/O读写函数:
(1)8位宽的字符串I/O操作
void insb(unsigned port,void * addr,unsigned long count);
void outsb(unsigned port ,void * addr,unsigned long count);
(2)16位宽的字符串I/O操作
void insw(unsigned port,void * addr,unsigned long count);
void outsw(unsigned port ,void * addr,unsigned long count);
(3)32位宽的字符串I/O操作
void insl(unsigned port,void * addr,unsigned long count);
void outsl(unsigned port ,void * addr,unsigned long count);
在一些平台上(典型地如X86),对于慢速外设来说,如果CPU读写其I/O端口的速度太快,可能会发生丢失数据。这就要在两次连续的I/O操作之间插入时延。Linux在io.h头文件中定义了带延迟的I/O读写函数,以XXX_p命名,如:inb_p()、outb_p()等。下面以out_p()为例进行分析。
将io.h中的宏定义__OUT(b,"b"char)展开后可得如下定义:
可以看出,outb_p()函数的实现中被插入了宏__FULL_SLOWN_DOWN_IO,以实现微小的延时。宏__FULL_SLOWN_DOWN_IO在头文件io.h中一开始就被定义:
显然,__FULL_SLOW_DOWN_IO就是一个或四个__SLOW_DOWN_IO(根据是否定义了宏REALLY_SLOW_IO来决定),而宏__SLOW_DOWN_IO则被定义成毫无意义的跳转语句或写端口0x80的操作(根据是否定义了宏SLOW_IO_BY_JUMPING来决定)。