Linux虚拟地址和物理地址的映射
➤背景
一般情况下,Linux系统中,进程的4GB内存空间被划分成为两个部分------用户空间和内核空间,大小分别为0~3G,3~4G。用户进程通常情况下,只能访问用户空间的虚拟地址,不能访问到内核空间。每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。3~4G之间的内核空间中,从低地址到高地址依次为:系统物理内存映射区—隔离带—vmalloc虚拟内存分配区—隔离带—高端内存映射区—专用页面映射区—保留区。
➤内核空间内存动态申请
主要包括三个函数:kmalloc(), __get_free_pages, vmalloc。
➣kmalloc(), __get_free_pages申请的内存位于物理地址映射区,而且在物理上也是连续的,返回的虚拟地址与真实的物理地址(物理地址是连续的,虚拟地址也是连续的)只有一个固定的偏移,因此存在较简单的转换关系。
➣而vmalloc申请的内存位于vmalloc虚拟内存分配区(这些区都是以线性地址为度量),它在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,而vmalloc申请的虚拟内存和物理内存之间也没有简单的换算关系。因为vmalloc申请的在虚拟内存空间连续的内存区在物理内存中并不一定连续,可以想象为了完成vmalloc,新的页表需要被建立,因此,调用vmalloc来分配少量内存是不妥的。一般来讲,kmalloc用来分配小于128K的内存,而更大的内存块需要用vmalloc来实现。
➤虚拟地址与物理地址关系
对于内核物理内存映射区的虚拟内存(用kmalloc(), __get_free_pages申请的),使用virt_to_phys()和phys_to_virt()来实现物理地址和内核虚拟地址之间的互相转换。它实际上,仅仅做了3G的地址移位。上述方法适用于常规内存(内核物理内存映射区),高端内存的虚拟地址与物理地址之间不存在如此简单的换算关系。因为它涉及到了分离物理页的页表控制机制。
➤ioremap
在ARM中,设备的寄存器或者存储块的这部分空间属于内存空间的一部分,我们称之为IO内存。在内核中访问IO内存之前,我们只有IO内存的物理地址,这样是无法通过软件直接访问的,需要首先用ioremap()函数将设备所处的物理地址映射到内核虚拟地址空间(3GB~4GB)。然后,才能根据映射所得到的内核虚拟地址范围,通过访问指令访问这些IO内存资源。在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。
➤mmap
用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。这种数据传输是直接的,不需要用到内核空间作为数据转移的中间站。remap_page_range()函数的功能是构造用于映射一段物理地址的新页表,实现了内核空间与用户空间的映射。在内核驱动程序的初始化阶段,通过ioremap()将物理地址映射到内核虚拟空间;在驱动程序的mmap系统调用中,使用remap_page_range()将该块ROM映射到用户虚拟空间。这样内核空间和用户空间都能访问这段被映射后的虚拟地址。
☢进程空间/内核空间/IO内存
其中,后面两个指的是同一段物理内存区域,只是一个为虚拟地址(内核空间),一个为物理地址(IO内存)。进程空间和内核空间对应着不同的物理地址,它们之间的数据传递,是实际的数据的拷贝。
☢进程空间/IO内存
其中,进程空间mmap得到的那段虚拟地址跟IO内存对应着同一段物理地址。这个过程没有额外的数据中转,读写都直接针对硬件的物理地址进行。
一般来讲,小数据量的传输用ioremap()就足够了,
➤IO内存的一般访问方法
➣首先是调用request_mem_region()申请资源,即告诉内核,本驱动正在使用这段物理内存,其他驱动不得访问它们。在设备驱动模块加载或open()函数中进行。
➣接着讲寄存器地址通过ioremap()映射到内核空间虚拟地址,之后就可以通过Linux设备访问编程接口访问这些设备的寄存器了。在设备驱动初始化、write(),read(),ioctl()函数中进行。
➣ 访问完成之后,应对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的IO内存资源。在设备驱动模块卸载或release()函数中进行。
➣linux中的物理地址和虚拟地址 :
在支持MMU的32位处理器平台上,Linux系统中的物理存储空间和虚拟存储空间的地址范围分别都是从0x00000000到0xFFFFFFFF,共4GB,但物理存储空间与虚拟存储空间布局完全不同。Linux运行在虚拟存储空间,并负责把系统中实际存在的远小于4GB的物理内存根据不同需求映射到整个4GB的虚拟存储空间中。
➤物理存储空间布局
Linux的物理存储空间布局与处理器相关,详细情况可以从处理器用户手册的存储空间分布表(memory map)相关章节中查到,我们这里只列出嵌入式处理器平台Linux物理内存空间的一般布局。
说明:
➣最大node号n不能大于MAX_NUMNODES-1。
➣MAX_NUMNODES表示系统支持的最多node数。在ARM系统中,Sharp芯片最多支持16个nodes,其他芯片最多支持4个nodes。
➣numnodes是当前系统中实际的内存node数。
➣在不支持CONFIG_DISCONTIGMEM选项的系统中,只有一个内存node。
➣最大bank号m不能大于NR_BANKS-1。
➣NR_BANKS表示系统中支持的最大内存bank数,一般等于处理器的RAM片选数。在ARM系统中,Sharp芯片最多支持16个banks,其他芯片最多支持8个banks。
➣mem_init()函数会将所有节点的页帧位码表所占空间、孔洞页描述符空间及空闲内存页都释放掉。
➤虚拟存储空间布局
在支持MMU的系统中,当系统做完硬件初始化后就使能MMU功能,这样整个系统就运行在虚拟存储空间中,实现虚拟存储空间到物理存储空间映射功能的是处理器的MMU,而虚拟存储空间与5路存储空间的映射关系则是由Linux内核来管理的。32位系统中物理存储空间占4GB空间,虚拟存储空间同样占4GB空间,Linux把物理空间中实际存在的远远小于4GB的内存空间映射到整个4GB虚拟存储空间中除映射I/O空间之外的全部空间,所以虚拟内存空间远远大于物理内存空间,这就说同一块物理内存可能映射到多处虚拟内存地址空间上,这正是Linux内存管理职责所在。
说明:
➣线性地址空间:是指Linux系统中从0x00000000到0xFFFFFFFF整个4GB虚拟存储空间。
➣内核空间:内核空间表示运行在处理器最高级别的超级用户模式(supervisor mode)下的代码或数据,内核空间占用从0xC000000到0xFFFFFFFF的1GB线性地址空间,内核线性地址空间由所有进程共享,但只有运行在内核态的进程才能访问,用户进程可以通过系统调用切换到内核态访问内核空间,进程运行在内核态时所产生的地址都属于内核空间。
➣用户空间:用户空间占用从0x00000000到0xBFFFFFFF共3GB的线性地址空间,每个进程都有一个独立的3GB用户空间,所以用户空间由每个进程独有,但是内核线程没有用户空间,因为它不产生用户空间地址。另外子进程共享(继承)父进程的用户空间只是使用与父进程相同的用户线性地址到物理内存地址的映射关系,而不是共享父进程用户空间。运行在用户态和内核态的进程都可以访问用户空间。
➣内核逻辑地址空间:是指从PAGE_OFFSET到high_memory之间的线性地址空间,是系统物理内存映射区,它映射了全部或部分(如果系统包含高端内存)物理内存。内核逻辑地址空间与图18-4中的系统RAM内存物理地址空间是一一对应的(包括内存孔洞也是一一对应的),内核逻辑地址空间中的地址与RAM内存物理地址空间中对应的地址只差一个固定偏移量,如果RAM内存物理地址空间从0x00000000地址编址,那么这个偏移量就是PAGE_OFFSET。
➣低端内存:内核逻辑地址空间所映射物理内存就是低端内存,低端内存在Linux线性地址空间中始终有永久的一一对应的内核逻辑地址,系统初始化过程中将低端内存永久映射到了内核逻辑地址空间,为低端内存建立了虚拟映射页表。低端内存内物理内存的物理地址与线性地址之间的转换可以通过__pa(x)和__va(x)两个宏来进行,__pa(x)将内核逻辑地址空间的地址x转换成对应的物理地址,相当于__virt_to_phys((unsigned long)(x)),__va(x)则相反,把低端物理内存空间的地址转换成对应的内核逻辑地址,相当于((void *)__phys_to_virt((unsigned long)(x)))。
➣高端内存:低端内存地址之上的物理内存是高端内存,高端内存在Linux线性地址空间中没有没有固定的一一对应的内核逻辑地址,系统初始化过程中不会为这些内存建立映射页表将其固定映射到Linux线性地址空间,而是需要使用高端内存的时候才为分配的高端物理内存建立映射页表,使其能够被内核使用,否则不能被使用。高端内存的物理地址于现行地址之间的转换不能使用上面的__pa(x)和__va(x)宏。
➣高端内存概念的由来:如上所述,Linux将4GB的线性地址空间划分成两部分,从0x00000000到0xBFFFFFFF共3GB空间作为用户空间由用户进程独占,这部分线性地址空间并没有固定映射到物理内存空间上;从0xC0000000到0xFFFFFFFF的第4GB线性地址空间作为内核空间,在嵌入式系统中,这部分线性地址空间除了映射物理内存空间之外还要映射处理器内部外设寄存器空间等I/O空间。0xC0000000~high_memory之间的内核逻辑地址空间专用来固定映射系统中的物理内存,也就是说0xC0000000~high_memory之间空间大小与系统的物理内存空间大小是相同的(当然在配置了CONFIG_DISCONTIGMEMD选项的非连续内存系统中,内核逻辑地址空间和物理内存空间一样可能存在内存孔洞),如果系统中的物理内存容量远小于1GB,那么内核现行地址空间中内核逻辑地址空间之上的high_memory~0xFFFFFFFF之间还有足够的空间来固定映射一些I/O空间。可是,如果系统中的物理内存容量(包括内存孔洞)小于1GB,那么就没有足够的内核线性地址空间来固定映射系统全部物理内存以及一些I/O空间了,为了解决这个问题,在x86处理器平台设置了一个经验值:896MB,就是说,如果系统中的物理内存(包括内存孔洞)大于896MB,那么将前896MB物理内存固定映射到内核逻辑地址空间0xC0000000~0xC0000000+896MB(=high_memory)上,而896MB之后的物理内存则不建立到内核线性地址空间的固定映射,这部分内存就叫高端物理内存。此时内核线性地址空间high_memory~0xFFFFFFFF之间的128MB空间就称为高端内存线性地址空间,用来映射高端物理内存和I/O空间。896MB是x86处理器平台的经验值,留了128MB线性地址空间来映射高端内存以及I/O地址空间,我们在嵌入式系统中可以根据具体情况修改这个阈值,比如,MIPS中将这个值设置为0x20000000B(512MB),那么只有当系统中的物理内存空间容量大于0x20000000B时,内核才需要配置CONFIG_HIGHMEM选项,使能内核对高端内存的分配和映射功能。什么情况需要划分出高端物理内存以及高端物理内存阈值的设置原则见上面的内存页区(zone)概念说明。
➣高端线性地址空间:从high_memory到0xFFFFFFFF之间的线性地址空间属于高端线性地址空间,其中VMALLOC_START~VMALLOC_END之间线性地址被vmalloc()函数用来分配物理上不连续但线性地址空间连续的高端物理内存,或者被vmap()函数用来映射高端或低端物理内存,或者由ioremap()函数来重新映射I/O物理空间。PKMAP_BASE开始的LAST_PKMAP(一般等于1024)页线性地址空间被kmap()函数用来永久映射高端物理内存。FIXADDR_START开始的KM_TYPE_NR*NR_CPUS页线性地址空间被kmap_atomic()函数用来临时映射高端物理内存,其他未用高端线性地址空间可以用来在系统初始化期间永久映射I/O地址空间。
➤嵌入式系统中如何访问I/O资源
几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:
➣I/O映射方式(I/O-mapped)
典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。
➣内存映射方式(Memory-mapped)
RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。
但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所内指令访问这些I/O得到的核心虚地址范围,通过访内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,原型如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
iounmap函数用于取消ioremap()所做的映射,原型如下:
void iounmap(void * addr);
这两个函数都是实现在mm/ioremap.c文件中。
在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。如在x86平台上,读写I/O的函数如下所示:
#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((a),__io_virt(b),(c)) #define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
最后,我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。