很多硬件设备都有自己的内存,通常称之为I/O空间。例如,所有比较新的图形卡都有几MB的RAM,称为显存,用它来存放要在屏幕上显示的屏幕影像。
1.地址映射
根据设备和总线类型的不同,PC体系结构中的I/O空间可以在三个不同的物理地址范围之间进行映射:
(1)对于连接到ISA总线上的大多数设备
I/O空间通常被映射到从0xa0000到0xfffff的物理地址范围,这就在640K和1MB之间留出了一段空间,这就是所谓的“洞”。
(2)对于使用VESA本地总线(VLB)的一些老设备
这是主要由图形卡使用的一条专用总线:I/O空间被映射到从0xe00000到0xffffff的地址范围中,也就是14MB到16MB之间。因为这些设备使页表的初始化更加复杂,因此已经不生产这种设备。
(3)对于连接到PCI总线的设备
I/O空间被映射到很大的物理地址区间,位于RAM物理地址的顶端。这种设备的处理比较简单。
2.访问I/O空间
内核如何访问一个I/O空间单元?让我们从PC体系结构开始入手,这个问题很容易就可以解决,之后我们再进一步讨论其他体系结构。
不要忘了内核程序作用于虚拟地址,因此I/O空间单元必须表示成大于PAGE_OFFSET的地址。在后面的讨论中,我们假设PAGE_OFFSET等于0xc0000000,也就是说,内核虚拟地址是在第
内核驱动程序必须把I/O空间单元的物理地址转换成内核空间的虚拟地址。在PC体系结构中,这可以简单地把32位的物理地址和0xc0000000常量进行或运算得到。例如,假设内核需要把物理地址为0x000b0fe4的I/O单元的值存放在t1中,把物理地址为0xfc000000的I/O单元的值存放在t2中,就可以使用下面的表达式来完成这项功能:
t1 = *((unsigned char *)(0xc00b0fe4));
t2 = *((unsigned char *)(0xfc000000));
在第六章我们已经介绍过,在初始化阶段,内核已经把可用的RAM物理地址映射到虚拟地址空间第
但是,对于第二个语句来说,这里有一个问题,因为其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空间单元读取1、2或者4个字节
writeb, writew, writel
分别向一个I/O空间单元写入1、2或者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空间所用方法的差异。