11.2.7 I/O 空间的映射

很多硬件设备都有自己的内存,通常称之为I/O空间。例如,所有比较新的图形卡都有几MBRAM,称为显存,用它来存放要在屏幕上显示的屏幕影像。

1.地址映射

    根据设备和总线类型的不同,PC体系结构中的I/O空间可以在三个不同的物理地址范围之间进行映射:

1)对于连接到ISA总线上的大多数设备

I/O空间通常被映射到从0xa00000xfffff的物理地址范围,这就在640K1MB之间留出了一段空间,这就是所谓的“洞”。

2)对于使用VESA本地总线(VLB)的一些老设备

    这是主要由图形卡使用的一条专用总线:I/O空间被映射到从0xe000000xffffff的地址范围中,也就是14MB16MB之间。因为这些设备使页表的初始化更加复杂,因此已经不生产这种设备。

3)对于连接到PCI总线的设备

    I/O空间被映射到很大的物理地址区间,位于RAM物理地址的顶端。这种设备的处理比较简单。

2.访问I/O空间

    内核如何访问一个I/O空间单元?让我们从PC体系结构开始入手,这个问题很容易就可以解决,之后我们再进一步讨论其他体系结构。

    不要忘了内核程序作用于虚拟地址,因此I/O空间单元必须表示成大于PAGE_OFFSET的地址。在后面的讨论中,我们假设PAGE_OFFSET等于0xc0000000,也就是说,内核虚拟地址是在第4G

    内核驱动程序必须把I/O空间单元的物理地址转换成内核空间的虚拟地址。在PC体系结构中,这可以简单地把32位的物理地址和0xc0000000常量进行或运算得到。例如,假设内核需要把物理地址为0x000b0fe4I/O单元的值存放在t1中,把物理地址为0xfc000000I/O单元的值存放在t2中,就可以使用下面的表达式来完成这项功能:

 

    t1 = *((unsigned char *)(0xc00b0fe4));

    t2 = *((unsigned char *)(0xfc000000));

 

    在第六章我们已经介绍过,在初始化阶段,内核已经把可用的RAM物理地址映射到虚拟地址空间第4G的最初部分。因此,分页机制把出现在第一个语句中的虚拟地址0xc00b0fe4映射回到原来的I/O物理地址0x000b0fe4,这正好落在从640K1MB的这段“ISA洞”中。这正是我们所期望的。

    但是,对于第二个语句来说,这里有一个问题,因为其I/O物理地址超过了系统RAM的最大物理地址。因此,虚拟地址0xfc000000就不需要与物理地址0xfc000000相对应。在这种情况下,为了在内核页表中包括对这个I/O物理地址进行映射的虚拟地址,必须对页表进行修改:这可以通过调用ioremap( )函数来实现。ioremap( )vmalloc( )函数类似,都调用get_vm_area( ) 建立一个新的vm_struct描述符,其描述的虚拟地址区间为所请求I/O空间区的大小。然后,ioremap( )函数适当地更新所有进程的对应页表项。

因此,第二个语句的正确形式应该为:

 

    io_mem = ioremap(0xfb000000, 0x200000);

    t2 = *((unsigned char *)(io_mem + 0x100000));

 

    第一条语句建立一个2MB的虚拟地址区间,从0xfb000000开始;第二条语句读取地址0xfc000000的内存单元。驱动程序以后要取消这种映射,就必须使用iounmap( )函数。

 

    现在让我们考虑一下除PC之外的体系结构。在这种情况下,把I/O物理地址加上0xc0000000常量所得到的相应虚拟地址并不总是正确的。为了提高内核的可移植性,Linux特意包含了下面这些宏来访问I/O空间

    readb, readw, readl

   分别从一个I/O空间单元读取12或者4个字节

   writeb, writew, writel

   分别向一个I/O空间单元写入12或者4个字节

   memcpy_fromio, memcpy_toio

   把一个数据块从一个I/O空间单元拷贝到动态内存中,另一个函数正好相反,把一个数据块从动态内存中拷贝到一个I/O空间单元

   memset_io

   用一个固定的值填充一个I/O空间区域

对于0xfc000000 I/O单元的访问推荐使用这样的方法:

    io_mem = ioremap(0xfb000000, 0x200000);

    t2 = readb(io_mem + 0x100000);

    使用这些宏就可以隐藏不同平台访问I/O空间所用方法的差异。