热门关键字:  ubuntu  分区  Fedora  linux系统进程  函数

当前位置 :| 主页>Linux教程>内核研究>

iomem

来源: 作者: 时间:2008-11-29 Tag: 点击:
Linux将基于I/O映射方式的I/O端口和基于内存映射方式的I/O端口资源统称为“I/O区域”(I/O Region)。I/O Region仍然是一种I/O资源,因此它仍然可以用resource结构类型来描述。下面我们就来看看Linux是如何管理I/O Region的。
  
    3.3.1 I/O Region的分配
  
    在函数__request_resource()的基础上,Linux实现了用于分配I/O区域的函数__request_region(),如下:
  
  
  struct resource * __request_region(struct resource *parent,
    unsigned long start, unsigned long n, const char *name)
  {
  struct resource *res = kmalloc(sizeof(*res), GFP_KERNEL);
  
  if (res) {
  memset(res, 0, sizeof(*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;
网管联盟bitsCN_com


  if (conflict != parent) {
  parent = conflict;
  if (!(conflict->flags & IORESOURCE_BUSY))
  continue;
  }
  
  /* Uhhuh, that didn't work out.. */
  kfree(res);
  res = NULL;
  break;
  }
  write_unlock(&resource_lock);
  }
  return res;
  }
  
  
  
  NOTE:
  
    ①首先,调用kmalloc()函数在SLAB分配器缓存中分配一个resource结构。
  
    ②然后,相应的根据参数?**跏蓟峙涞膔esource结构。注意!flags成员被初始化为IORESOURCE_BUSY。
  
    ③接下来,用一个for循环开始进行资源分配,循环体的步骤如下:
  
    l 首先,调用__request_resource()函数进行资源分配。如果返回NULL,说明分配成功,因此就执行break语句推出for循环,返回所分配的resource结构的指针,函数成功地结束。
  
    l 如果__request_resource()函数分配不成功,则进一步判断所返回的冲突资源节点是否就是父资源节点parent。如果不是,则将分配行为下降一个层次,即试图在当前冲突的资源节点中进行分配(只有在冲突的资源节点没有设置IORESOURCE_BUSY的情况下才可以),于是让 parent指针等于conflict,并在conflict->flags&IORESOURCE_BUSY为0的情况下执行 continue语句继续for循环。
网管联盟bitsCN_com


  
    l 否则如果相冲突的资源节点就是父节点parent,或者相冲突资源节点设置了IORESOURCE_BUSY标志位,则宣告分配失败。于是调用kfree ()函数释放所分配的resource结构,并将res指针置为NULL,最后用break语句推出for循环。
  
    ④最后,返回所分配的resource结构的指针。
  
    3.3.2 I/O Region的释放
  
    函数__release_region()实现在一个父资源节点parent中释放给定范围的I/O Region。实际上该函数的实现思想与__release_resource()相类似。其源代码如下:
  
  
  void __release_region(struct resource *parent,
      unsigned long start, unsigned long n)
  {
  struct resource **p;
  unsigned long end;
  
  p = &parent->child;
  end = start + n - 1;
  
  for (;;) {
  struct resource *res = *p;
  
  if (!res)
  break;
  if (res->start <= start && res->end >= end) {
  if (!(res->flags & IORESOURCE_BUSY)) {
  p = &res->child;
  continue;
  } 网管bitscn_com
  if (res->start != start' 'res->end != end)
  break;
  *p = res->sibling;
  kfree(res);
  return;
  }
  p = &res->sibling;
  }
  printk(Trying to free nonexistent resource <%08lx-%08lx>
  , start, end);
  }
  
  
  
    类似地,该函数也是通过一个for循环来遍历父资源parent的child链表。为此,它让指针res指向当前正被扫描的子资源节点,指针p指向前一个子资源节点的sibling成员变量,p的初始值为指向parent->child。For循环体的步骤如下:
  
    ①让res指针指向当前被扫描的子资源节点(res=*p)。
  
    ②如果res指针为NULL,说明已经扫描完整个child链表,所以退出for循环。
  
    ③如果res指针不为NULL,则继续看看所指定的I/O区域范围是否完全包含在当前资源节点中,也即看看[start,start+n-1]是否包含在res->[start,end]中。如果不属于,则让p指向当前资源节点的sibling成员,然后继续for循环。如果属于,则执行下列步骤:
  
    l 先看看当前资源节点是否设置了IORESOURCE_BUSY标志位。如果没有设置该标志位,则说明该资源节点下面可能还会有子节点,因此将扫描过程下降一个层次,于是修改p指针,使它指向res->child,然后执行continue语句继续for循环。
网管有家bitscn.net


  
    l 如果设置了IORESOURCE_BUSY标志位。则一定要确保当前资源节点就是所指定的I/O区域,然后将当前资源节点从其父资源的child链表中去除。这可以通过让前一个兄弟资源节点的sibling指针指向当前资源节点的下一个兄弟资源节点来实现(即让*p=res->sibling),最后调用kfree()函数释放当前资源节点的resource结构。然后函数就可以成功返回了。
  
    3.3.3 检查指定的I/O Region是否已被占用
  
    函数__check_region()检查指定的I/O Region是否已被占用。其源代码如下:
  
  
  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;
  }
  
  
  
    该函数的实现与__check_resource()的实现思想类似。首先,它通过调用__request_region()函数试图在父资源 parent中分配指定的I/O Region。如果分配不成功,将返回NULL,因此此时函数返回错误值-EBUSY表示所指定的I/O Region已被占用。如果res指针不为空则说明所指定的I/O Region没有被占用。于是调用__release_resource()函数将刚刚分配的资源释放掉(实际上是将res结构从parent的 child链表去除),然后调用kfree()函数释放res结构所占用的内存。最后,返回0值表示指定的I/O Region没有被占用。 中国网管论坛bbs.bitsCN.com
  
  3.4 管理I/O端口资源
  
    我们都知道,采用I/O映射方式的X86处理器为外设实现了一个单独的地址空间,也即“I/O空间”(I/O Space)或称为“I/O端口空间”,其大小是64KB(0x0000-0xffff)。Linux在其所支持的所有平台上都实现了“I/O端口空间” 这一概念。
  
    由于I/O空间非常小,因此即使外设总线有一个单独的I/O端口空间,却也不是所有的外设都将其I/O端口(指寄存器)映射到“I/O端口空间”中。比如,大多数PCI卡都通过内存映射方式来将其I/O端口或外设内存映射到CPU的RAM物理地址空间中。而老式的 ISA卡通常将其I/O端口映射到I/O端口空间中。
  
    Linux是基于“I/O Region”这一概念来实现对I/O端口资源(I/O-mapped 或 Memory-mapped)的管理的。
  
    3.4.1 资源根节点的定义
  
    Linux在kernel/Resource.c文件中定义了全局变量ioport_resource和iomem_resource,来分别描述基于I/O映射方式的整个I/O端口空间和基于内存映射方式的I/O内存资源空间(包括I/O端口和外设内存)。其定义如下:
  
  
  struct resource ioport_resource =
      { PCI IO, 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO }; 网管论坛bbs_bitsCN_com
  struct resource iomem_resource =
      { PCI mem, 0x00000000, 0xffffffff, IORESOURCE_MEM };
  
  
  
    其中,宏IO_SPACE_LIMIT表示整个I/O空间的大小,对于X86平台而言,它是0xffff(定义在include/asm-i386/io.h头文件中)。显然,I/O内存空间的大小是4GB。
  
    3.4.2 对I/O端口空间的操作
  
    基于I/O Region的操作函数__XXX_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O端口空间进行操作的宏:①request_region()宏,请求在I/O端口空间中分配指定范围的I/O端口资源。②check_region()宏,检查 I/O端口空间中的指定I/O端口资源是否已被占用。③release_region()宏,释放I/O端口空间中的指定I/O端口资源。这三个宏的定义如下:
  
  
  #define request_region(start,n,name)
  __request_region(&ioport_resource, (start), (n), (name))
  #define check_region(start,n)
  __check_region(&ioport_resource, (start), (n))
  #define release_region(start,n)
  __release_region(&ioport_resource, (start), (n))
  
  
  
网管有家bitscn.net


    其中,宏参数start指定I/O端口资源的起始物理地址(是I/O端口空间中的物理地址),宏参数n指定I/O端口资源的大小。
  
    3.4.3 对I/O内存资源的操作
  
    基于I/O Region的操作函数__XXX_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O内存资源进行操作的宏:①request_mem_region()宏,请求分配指定的I/O内存资源。②check_ mem_region()宏,检查指定的I/O内存资源是否已被占用。③release_ mem_region()宏,释放指定的I/O内存资源。这三个宏的定义如下:
  
  
  #define request_mem_region(start,n,name)
    __request_region(&iomem_resource, (start), (n), (name))
  #define check_mem_region(start,n)
  __check_region(&iomem_resource, (start), (n))
  #define release_mem_region(start,n)
  __release_region(&iomem_resource, (start), (n))
  
  
  
    其中,参数start是I/O内存资源的起始物理地址(是CPU的RAM物理地址空间中的物理地址),参数n指定I/O内存资源的大小。
  
    3.4.4 对/proc/ioports和/proc/iomem的支持
网管网www_bitscn_com

  
    Linux在ioport.h头文件中定义了两个宏:
  
    get_ioport_list()和get_iomem_list(),分别用来实现/proc/ioports文件和/proc/iomem文件。其定义如下:
  
  
  #define get_ioport_list(buf) get_resource_list(&ioport_resource, buf, PAGE_SIZE)
  #define get_mem_list(buf) get_resource_list(&iomem_resource, buf, PAGE_SIZE)
  
  
  
  3.5 访问I/O端口空间
  
    在驱动程序请求了I/O端口空间中的端口资源后,它就可以通过CPU的IO指定来读写这些I/O端口了。在读写I/O端口时要注意的一点就是,大多数平台都区分8位、16位和32位的端口,也即要注意I/O端口的宽度。
  
    Linux在include/asm/io.h头文件(对于i386平台就是include/asm-i386/io.h)中定义了一系列读写不同宽度I/O端口的宏函数。如下所示:
  
    ⑴读写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端口空间的大小来决定的。 网管朋友网www_bitscn_net
  
    ⑵读写16位宽的I/O端口
  
  
    unsigned short inw(unsigned port);
    void outw(unsigned short value,unsigned port);
  
  
  
    ⑶读写32位宽的I/O端口
  
  
    unsigned int inl(unsigned port);
    void outl(unsigned int value,unsigned port);
  
  
  
    3.5.1 对I/O端口的字符串操作
  
    除了上述这些“单发”(single-shot)的I/O操作外,某些CPU也支持对某个I/O端口进行连续的读写操作,也即对单个I/O端口读或写一系列字节、字或32位整数,这就是所谓的“字符串I/O指令”(String Instruction)。这种指令在速度上显然要比用循环来实现同样的功能要快得多。
  
    Linux同样在io.h文件中定义了字符串I/O读写函数:
  
    ⑴8位宽的字符串I/O操作
  
  
    void insb(unsigned port,void * addr,unsigned long count);
    void outsb(unsigned port ,void * addr,unsigned long count);
  
  
  
    ⑵16位宽的字符串I/O操作
   中国网管论坛bbs.bitsCN.com
  
    void insw(unsigned port,void * addr,unsigned long count);
    void outsw(unsigned port ,void * addr,unsigned long count);
  
  
  
    ⑶32位宽的字符串I/O操作
  
  
    void insl(unsigned port,void * addr,unsigned long count);
    void outsl(unsigned port ,void * addr,unsigned long count);
  
  
  
    3.5.2 Pausing I/O
  
  
    在一些平台上(典型地如X86),对于老式总线(如ISA)上的慢速外设来说,如果CPU读写其I/O端口的速度太快,那就可能会发生丢失数据的现象。对于这个问题的解决方法就是在两次连续的I/O操作之间插入一段微小的时延,以便等待慢速外设。这就是所谓的“Pausing I/O”。
  
    对于Pausing 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) { 网管u家u.bitsCN.com
  __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来决定)。 网管朋友网www_bitscn_net
  
  3.6 访问I/O内存资源
  
    尽管I/O端口空间曾一度在x86平台上被广泛使用,但是由于它非常小,因此大多数现代总线的设备都以内存映射方式(Memory-mapped)来映射它的I/O端口(指I/O寄存器)和外设内存。基于内存映射方式的I/O端口(指I/O寄存器)和外设内存可以通称为“I/O内存”资源(I/O Memory)。因为这两者在硬件实现上的差异对于软件来说是完全透明的,所以驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是 “I/O内存”资源。
  
    从前几节的阐述我们知道,I/O内存资源是在CPU的单一内存物理地址空间内进行编址的,也即它和系统RAM同处在一个物理地址空间内。因此通过CPU的访内指令就可以访问I/O内存资源。
  
    一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,这可以通过系统固件(如BIOS)在启动时分配得到,或者通过设备的硬连线(hardwired)得到。比如,PCI卡的I/O内存资源的物理地址就是在系统启动时由PCI BIOS分配并写到PCI卡的配置空间中的BAR中的。而ISA卡的I/O内存资源的物理地址则是通过设备硬连线映射到640KB-1MB范围之内的。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,因为它们是在系统启动后才已知的(某种意义上讲是动态的),所以驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。
中国网管联盟bitsCN.com


  
    3.6.1 映射I/O内存资源
  
    Linux在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文件中。具体实现可参考《情景分析》一书。
  
    3.6.2 读写I/O内存资源
  
    在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写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))
网管u家www.bitscn.net

  #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((a),__io_virt(b),(c))
  #define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
  
   上述定义中的宏__io_virt()仅仅检查虚地址addr是否是核心空间中的虚地址。该宏在内核2.4.0中的实现是临时性的。具体的实现函数在arch/i386/lib/Iodebug.c文件。
  
    显然,在x86平台上访问I/O内存资源与访问系统主存RAM是无差别的。但是为了保证驱动程序的跨平台的可移植性,我们应该使用上面的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。


相关文章:
精通initramfs构建step by step
Linux利用kexec迅速切换内核
进程上下文VS中断上下文
内核通知链 学习笔记
linux spi子系统驱动分析
menuconfig 配置选项
《Linux操作系统内核实习》之练习一
udev详解
什么叫微内核,宏内核?
Linux 信号signal处理机制
开发简单的 Linux2.6 内核模块
删除内核的perl脚本
Linux2.6内核usb gadget驱动移植
GCC hacks in the Linux kernel
iomem
kernel学习的想法
让自己的驱动支持udev
linux内核编译步骤
内核的等待队列
Linux内核wait_queue深入分析
升级和删除内核
SD卡驱动分析2
Linux Kernel VDSO本地权限提升漏洞
内核中的TCP的追踪分析-15-TCP(IPV4)的客户端与
linux 2.6内核可加载模块的编译
内核模块HelloWorld
在环回接口上发送一个数据报
ARP初始化
1分钟编译FreeBSD内核
linux设备模型之uart驱动架构分析