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内核设计与实现 pdf➣低端内存:内核逻辑地址空间所映射物理内存就是低端内存,低端内存在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映射⼀个设备,意味着使⽤户空间的⼀段地址关联到设备内存上,这使得只要程序在分配的地址范围内进⾏读取或者写⼊,实际上就是对设备的访问。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论