Chinaunix首页 | 论坛 | 博客
  • 博客访问: 140097
  • 博文数量: 36
  • 博客积分: 127
  • 博客等级: 入伍新兵
  • 技术积分: 185
  • 用 户 组: 普通用户
  • 注册时间: 2012-08-05 00:06
文章分类

全部博文(36)

文章存档

2015年(1)

2014年(1)

2013年(3)

2012年(31)

我的朋友

分类:

2012-09-07 15:46:45

计算机通过读写设备上的寄存器来控制设备的。这些外设寄存器又称为"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 kernel io resource manage 01.gif
图4 资源A、B、C和D组成的资源二叉树
资源数据结构

Linux设计了一个通用的数据结构resource(又称为资源结构或资源描述子)来描述各种I/O资源(如:I/O端口、外设内存、DMA和IRQ等)。利用二叉树链表将不同类型的I/O资源分别连接成二叉树,二叉树的层次关系也反映了资源的层次关系。资源管理的数据结构(include/linux/ioport.h)如下:

struct resource {    /*表示资源的起始地址start和终止地址end,是一个闭区间[start,end]*/ resource_size_t start; //resource_size_t根据编译配置定义为u32或u64 resource_size_t end; const char *name; //指向此资源的名称 /*用以描述资源的属性。比如:资源的类型、是否只读、是否可缓存*/ unsigned long flags; /*二叉树的父亲、兄弟和孩子指针,以树的形式分层管理I/O资源*/ struct resource *parent, *sibling, *child; };

PCI总线的资源还用链表结构resource_list链接成链表,结构resource_list列出如下:

struct resource_list { struct resource_list *next; struct resource *res; struct pci_dev *dev; };

例如,I/O端口资源ioport_resource定义如下(在kernel/resource.c中):

struct resource ioport_resource = { .name = "PCI IO", .start = 0, .end = IO_SPACE_LIMIT,    //定义为0xffff,即64K .flags = IORESOURCE_IO,  //资源类型为IO };

Linux用树形结构来管理每一类I/O资源(如:I/O端口、外设内存、DMA和IRQ)。每一类I/O资源都对应一个资源树,树中的每一个节点都是一个resource结构。

资源的申请

函数request_resource申请并保留一个I/O或内存资源。参数root为根资源描述子,参数new为调用者期望得到的资源描述子。如果申请成功,函数返回0,否则,返回错误代码。

函数request_resource列出如下(在kernel/resource.c中):

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; }

函数__request_resource完成资源分配工作。如果参数new所描述的资源中的一部分或全部已经被其它节点所占用,则函数返回与new相冲突的资源root。否则,再遍历root的孩子,根据资源值范围将new插入到root的孩子链表就返回NULL。

函数__request_resource列出如下(在kernel/Resource.c中):

/* 如果不能请求到资源,返回冲突的资源条目*/ 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; /*资源值范围有重叠,即有冲突时,返回root*/ if (start < root->start)  return root; if (end > root->end) return root; p = &root->child; for (;;) {//遍历二叉树的孩子链表 tmp = *p;     /*如果new的资源值范围在节点资源值范围的前面或节点为空时,将新资源插入二叉树中*/ if (!tmp || tmp->start > end) { new->sibling = tmp; *p = new; new->parent = root; //父指针指向root return NULL; } p = &tmp->sibling; if (tmp->end < start) /*new的资源值范围在节点资源值范围的后面*/ continue; return tmp; } }

释放资源

函数release_resource释放以前保留的资源,参数old为旧资源的指针。其列出如下(在kernel/resource.c中):

int release_resource(struct resource *old) { int retval;   write_lock(&resource_lock); retval = __release_resource(old); write_unlock(&resource_lock); return retval; }

函数__release_resource遍历孩子链表,如果找到资源节点,就从链表中删除。其列出如下:

static int __release_resource(struct resource *old) { struct resource *tmp, **p;   p = &old->parent->child; //得到孩子链表头 /*遍历孩子链表,从链表删除old*/ for (;;) {    tmp = *p; //tmp指向当前扫描的节点 if (!tmp)   //tmp为空,说明已遍历完整个孩子链表 break; if (tmp == old) { //节点为旧的资源,从二叉树中摘除节点 *p = tmp->sibling; //当前节点变为兄弟节点(即下一个节点) old->parent = NULL; //父指针置为空 return 0; } p = &tmp->sibling;  //继续扫描下一个元素 } return -EINVAL; }

分配资源

函数allocate_resource在资源树的孩子节点之间查找适合指定条件的物理内存空洞给new,分配的区域还应字节对齐。然后,它再申请资源new,查看new是否与资源树中的资源相冲突,如果没有冲突,就将new加入到资源树中。

函数allocate_resource的参数说明如下:

root: 根资源描述子。

new: 调用者期望分配的资源描述子。

size: 请求的资源区域大小。

min: 需要分配的最小尺寸。

max: 需要分配的最大尺寸。

align: 用来对齐请求区域的字节数。

alignf: 可选的对齐函数。

alignf_data: 传递给对齐函数的数据。

函数allocate_resource列出如下(在kernel/resource.c中):

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; }

函数find_resource从孩子链表的孩子之间找到适合给定条件的资源空洞,赋给new。该函数列出如下:

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;  //this为当前节点,它指向孩子链表头   new->start = root->start;   /*跳过一个从0开始已分配的资源,因为this->start - 1向new->end下对齐将引起下溢*/ if (this && this->start == 0) { new->start = this->end + 1; //跳过this资源 this = this->sibling;    //得到下一个资源 } for(;;) { //遍历孩子链表,在孩子之间找到合适大小的资源空洞 if (this) new->end = this->start - 1; else new->end = root->end;   /*确定new的资源范围在[min, max]之内*/ 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);   /*确定new的资源大小不超过size*/ 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; }

I/O区域资源资源管理

不同的CPU构架,I/O端口可能基于I/O端口地址方式或者基于存储器地址方式,Linux将这两种方式的I/O端口资源统一称作为"I/O区域(I/O Region)"。I/O区域资源用结构resource描述。

I/O区域类型

I/O端口有两种地址方式,中断和DMA是两种较特殊的端口资源,Linux内核将它们单独定义出来,其实,它们也只是两种地址方式之一。I/O端口的类型定义如下(在include/linux/ioport.h中):

#define IORESOURCE_IO 0x00000100 /* I/O端品地址方式的端口*/ #define IORESOURCE_MEM 0x00000200  /*存储器地址方式的端口*/ #define IORESOURCE_IRQ 0x00000400 /*中断端口*/ #define IORESOURCE_DMA 0x00000800 /*DMA端口*/

为了方便操作,Linux内核为两种地址方式的端口分别提供了操作宏,它们定义如下(在include/linux/ioport.h中):

/*申请I/O区域*/ #define request_region(start,n,name) __request_region(&ioport_resource, (start), (n), (name� #define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name�   /* 释放I/O区域*/ #define release_region(start,n) __release_region(&ioport_resource, (start), (n� #define check_mem_region(start,n) __check_region(&iomem_resource, (start), (n� #define release_mem_region(start,n) __release_region(&iomem_resource, (start), (n�

从上面的宏定义可以看出,Linux内核为两种地址方式的I/O区域提供了统一的I/O区域操作函数,I/O区域操作函数又通过资源管理函数API完成具体的资源管理的。

两种地址方式的I/O区域的资源树根节点分别列出如下(在kernel/resource.c中):

struct resource ioport_resource = { .name = "PCI IO", .start = 0, .end = IO_SPACE_LIMIT,  //定义为0xffff,即64K .flags = IORESOURCE_IO, //资源类型为I/O端口地址方式 };   struct resource iomem_resource = { .name = "PCI mem", .start = 0, .end = -1, .flags = IORESOURCE_MEM, //资源类型为存储器地址方式 };

从上述资源定义可以看出,I/O端口地址方式的I/O地址空间很小,一般小于64K,而存储器地址方式的I/O地址可以设计得很大。它们的起始地址为0。

Linux内核还将不同类型的虚拟地址空间作为资源管理,定义为存储器地址方式的资源。

分配I/O区域

函数__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中):

struct resource * __request_region(struct resource *parent, resource_size_t start, resource_size_t n, const char *name) {   /*分配资源,并初始化*/ struct resource *res = kzalloc(sizeof(*res), GFP_KERNEL);   if (res) { res->name = name; //设置资源名 res->start = start; //设置资源起始地址 res->end = start + n - 1; //设置资源结束地址 res->flags = IORESOURCE_BUSY; //将资源标识为忙   write_lock(&resource_lock);        /*遍历资源树,看是否与树中节点中资源范围是否有冲突*/ for (;;) { struct resource *conflict;   conflict = __request_resource(parent, res); if (!conflict) //如果没有冲突,跳出循环 break; if (conflict != parent) { //如果冲突不是父节点,即是子节点 parent = conflict; if (!(conflict->flags & IORESOURCE_BUSY))//如果冲突的资源不忙时,继续循环 continue; }   /* 运行到此,说明冲突是父节点或者冲突的子节点正忙,此时,无法申请资源,返回NULL*/ kfree(res); res = NULL; break; } write_unlock(&resource_lock); } return res; }

检查I/O区域是否占用

函数__check_region()检查给定的I/O区域是否被被占用。该函数列出如下:

int __check_region(struct resource *parent, unsigned long start, unsigned long n) { struct resource * res;   res = __request_region(parent, start, n, "check-region"); if (!res) return -EBUSY;   release_resource(res); kfree(res); return 0; }

释放I/O区域

函数__release_region释放以前保留的资源区域,并从资源树中删除资源区域所对应的节点。参数parent为父资源描述子,参数start为资源起始地址,参数n为资源区域大小。

函数__release_region遍历资源树,如果释放的地址区域在当前节点res的地址范围之内,且节点资源不忙,就将孩子节点设为当前节点,继续遍历树的下一层。否则,继续遍历孩子链表。找到指定区域的资源节点后,从树中删除该节点。

函数__release_region列出如下:

void __release_region(struct resource *parent, resource_size_t start, resource_size_t n) { struct resource **p; resource_size_t end;   p = &parent->child; end = start + n - 1;   write_lock(&resource_lock);   for (;;) {  //遍历资源树 struct resource *res = *p;   if (!res) //如果当前节点res为空,终止循环 break;        /*释放的地址区域在当前节点res的地址范围之内*/ if (res->start <= start && res->end >= end) {  if (!(res->flags & IORESOURCE_BUSY)) { //节点资源不忙 p = &res->child;  //将当前节点的孩子节点设为当前节点 continue; //继续循环 } if (res->start != start || res->end != end) break; *p = res->sibling; //将当前节点指向下一个兄弟,即删除给定范围的资源节点 write_unlock(&resource_lock); kfree(res); return; } p = &res->sibling;  //将当前节点的兄弟设置为当前节点,以便遍历兄弟节点 }   write_unlock(&resource_lock); }

I/O区域在/proc文件系统的显示

Linux将I/O区域信息显示在/proc/iomem和/proc/ioports文件中,它们分别对应存储器地址区域和I/O端口地址区域,这两个文件内容列出如下:

#cat /proc/iomem 00000000-0009d7ff : System RAM //保留区域,大小为640K 00000000-00000000 : Crash kernel 0009d800-0009ffff : reserved //保留区域,大小为10K 000cdc00-000cffff : pnp 00:0e 000f0000-000fffff : reserved //保留区域,大小为64K 00100000-3beeffff : System RAM 00200000-0044eaee : Kernel code //内核代码区域,约2.4M 0044eaef-005aca1f : Kernel data //内核数据区域,约1.4M 3bef0000-3bef2fff : ACPI Non-volatile Storage 3bef3000-3befffff : ACPI Tables 3c000000-3fffffff : reserved //保留64M 50000000-5001ffff : 0000:00:05.0 //约128K,与下一条目之间的地址空间为用户空间,约2.68G e0000000-efffffff : 0000:00:05.0  //约268M f0000000-f3ffffff : reserved //保留64M fb000000-fbffffff : 0000:00:05.0 fc000000-fcffffff : 0000:00:05.0 fd700000-fd7fffff : PCI Bus #01 fd800000-fd8fffff : PCI Bus #03 fd900000-fd9fffff : PCI Bus #03 fda00000-fdafffff : PCI Bus #04   #cat /proc/ioports 0000-001f : dma1 0020-0021 : pic1 0040-0043 : timer0 0050-0053 : timer1 0060-006f : keyboard 0070-0073 : rtc0 0080-008f : dma page reg 00a0-00a1 : pic2 00c0-00df : dma2 00f0-00ff : fpu 0170-0177 : 0000:00:0d.0 0170-0177 : libata 01f0-01f7 : 0000:00:0d.0 01f0-01f7 : libata 02f8-02ff : serial 0376-0376 : 0000:00:0d.0 0376-0376 : libata 0378-037a : parport0 03c0-03df : vga+ 03f2-03f5 : floppy

文件/proc/iomem显示了存储器地址区域,它是各种虚拟地址空间的分配,包括设备物理地址空间在虚拟地址空间的映射、用户空间、内核代码与数据空间、保留区域等。保留区域直到隔离带保护作用。

对于x86构架,文件/proc/ioports显示了I/O端口物理地址空间(一般为64K),它独立于内存空间,Linux内核通过专门的I/O端口访问指令访问这段物理地址空间。

系统上物理设备的端口空间按存储器地址方式或I/O端口地址方式进行访问。对于x86构架来说,CPU提供端口控制的设备,采用I/O端口地址方式,其他外围设备,采用存储器地址方式。

中断控制器资源管理

高级可编程中断控制器(Advanced Programmable Interrupt Controller)

static struct resource *ioapic_resources; static struct resource * __init ioapic_setup_resources(void) { #define IOAPIC_RESOURCE_NAME_SIZE 11 unsigned long n; struct resource *res; char *mem; int i;   if (nr_ioapics <= 0) return NULL;   n = IOAPIC_RESOURCE_NAME_SIZE + sizeof(struct resource); n *= nr_ioapics;   mem = alloc_bootmem(n); res = (void *)mem;   if (mem != NULL) { memset(mem, 0, n); mem += sizeof(struct resource) * nr_ioapics;   for (i = 0; i < nr_ioapics; i++) { res[i].name = mem; res[i].flags = IORESOURCE_MEM | IORESOURCE_BUSY; sprintf(mem, "IOAPIC %u", i); mem += IOAPIC_RESOURCE_NAME_SIZE; } }   ioapic_resources = res;   return res; }   static int __init ioapic_insert_resources(void) { int i; struct resource *r = ioapic_resources;   if (!r) { printk("IO APIC resources could be not be allocated.\n"); return -1; }   for (i = 0; i < nr_ioapics; i++) { insert_resource(&iomem_resource, r); r++; }   return 0; }


I/O端口读写函数
映射I/O内存资源

 驱动程序不能直接访问物理内存,为了让软件可以访问 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内存资源

  由于在某些平台上,对I/O内存和系统内存有不同的访问处理,因此为了确保跨平台的兼容性,Linux实现了一系列读写I/O内存资源的函数,这些函数在不同的平台上有不同的实现。但在x86平台上,读写I/O内存与读写RAM无任何差别。如下所示(include/asm-i386/io.h):

#define readb(addr) (*(volatile unsigned char *) __io_virt(addr� #define readw(addr) (*(volatile unsigned short *) __io_virt(addr� #define readl(addr) (*(volatile unsigned int *) __io_virt(addr�   #define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b� #define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b� #define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b�   #define memset_io(a,b,c) memset(__io_virt(a),(b),(c� #define memcpy_fromio(a,b,c) memcpy #define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c�

  上述定义中的宏__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)展开后可得如下定义:

extern inline void outb(unsigned char value, unsigned short port) { __asm__ __volatile__ ("outb %" "b " "0,%" "w" "1" : : "a" (value), "Nd" (port)); } extern inline void outb_p(unsigned char value, unsigned short port) { __asm__ __volatile__ ("outb %" "b " "0,%" "w" "1" __FULL_SLOW_DOWN_IO : : "a" (value), "Nd" (port)); }

可以看出,outb_p()函数的实现中被插入了宏__FULL_SLOWN_DOWN_IO,以实现微小的延时。宏__FULL_SLOWN_DOWN_IO在头文件io.h中一开始就被定义:

#ifdef SLOW_IO_BY_JUMPING #define __SLOW_DOWN_IO " jmp 1f 1: jmp 1f 1:" #else #define __SLOW_DOWN_IO " outb %%al,$0x80" #endif   #ifdef REALLY_SLOW_IO #define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO   __SLOW_DOWN_IO __SLOW_DOWN_IO __SLOW_DOWN_IO #else #define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO #endif

  显然,__FULL_SLOW_DOWN_IO就是一个或四个__SLOW_DOWN_IO(根据是否定义了宏REALLY_SLOW_IO来决定),而宏__SLOW_DOWN_IO则被定义成毫无意义的跳转语句或写端口0x80的操作(根据是否定义了宏SLOW_IO_BY_JUMPING来决定)。

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