显卡地址与cpu虚存地址的映射
1 问题提出
在通过内核读取nvidia显卡温度的过程中遇到下述代码
/* resource 0 is mmio regs */
/* resource 1 is linear FB */
/* resource 2 is RAMIN (mmio regs + 0x1000000) */
/* resource 6 is bios */
/* map the mmio regs */
resource_size_t mmio_start_offs = pci_resource_start(struct pci_dev*,int offset); //返回显卡设备的I/0内存地址区间[寄存器地址空间],这部分地址区间对应了显卡上的寄存器
void __iomem* mmio = ioremap(mmio_start_offs, 0x00800000); //将显卡地址空间映射到cpu上的虚拟地址区间mmio上,即建立内核页表,实现内核虚拟地址-->显卡寄存器地
在程序测试过程中,显示外设的I/O内存资源物理地址为:mmio_start_offs=0xfd000000,而映射的cpu虚存地址为:mmio=0xfb380000,由于测试机物理内存为1G,所以看以看出外设的寄存器地址区间在物理地址之外,但是通过页面映射转化到4G虚拟内存内部,从而通过一个页表可以访问实际内存、也可以访问外设寄存器,另外通过cat /proc/iomem可以看出对于物理地址范围fd000000-fdffffff对应的设备为0000:01:00.0,而通过lspci -s 0000:01:00.0[依次为:domain:bus:slot.func]显示该pci设备正好是nvidia显卡设备。至于如何通过转化访问外设寄存器,那是由硬件自动完成的,对系统是透明的。引用:在内存映射方式中,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源
同时可以看出,通过上述两行代码,实现了通过进程虚拟地址访问显卡设备寄存器的功能,那么内核究竟怎样访问外设的I/O端口(寄存器)?
几乎每一种外设访问都是通过读写设备上的寄存器来进行的,设备寄存器包括控制寄存器、状态寄存器和数据寄存器三大类;外设的寄存器通常被连续地编址。根据CPU体系结构和外设的不同,CPU对IO端口(寄存器)的编址方式有两种:
(1)I/O映射方式(I/O-mapped)
典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间"。这种情况下,进程内存地址空间和设备地址空间是独立的,CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。比如在前文cpu温度的检测过程中,最终通过一条特定的汇编指令:asm volatile("rdmsr" : "A"(val) : "c" (msr))完成cpu
温度对应的寄存器数值的读取过程,进程无法通过访存指令访问到该寄存器值。
(2)内存映射方式(Memory-mapped)
RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。比如对于上述中显卡温度传感器对应寄存器的访问,首先将显卡寄存器地址空间映射到进程虚拟地址空间中,然后通过访问虚拟地址空间的相对偏移即可访问到显卡上的对应寄存器,这种情况下,显然显卡上各个寄存器的地址是连续的、次序是固定的、大小也是固定的;
关于这两种映射方式详细介绍以及附录部分内容参考:http://dev.yesky.com/412/2639912.shtml
2 关于ioremap_nocache实现过程:即将总线地址(总线地址即物理地址)映射到进程虚拟地址过程
该部分详细内容可参考:http://bbs.chinaunix.net/thread-2296027-1-1.html
2.1 映射原理分析:
1 确定显卡物理地址大于high_memory[1G内存中大约为896M],即位于非连续映射地址空间区域
非连续映射地址空间用于将连续的线性地址映射到不连续的物理地址!同时它也提供了一种访问高物理内存的方法!
内核主要在一下三种情况使用非连续映射地址空间
映射设备的I/O空间
为内核模块分配空间
为交换分区分配空间
非连续映射地址空间的起始地址在常规映射地址空间的结束地址后8MB-16MB之间,而且保证8MB对齐(地址的低24位为0)
2 在进程虚拟地址空间中给显卡地址空间分配一个vm_area_struct虚存区间结构,并且建立相应的页表结构。
3 将其加入到进程的虚存结构mm中
2.2 映射后具体图示如下
可见,通过HOST BRIDGE硬件控制了对显卡温度寄存器和cpu温度的访问,通过lspci -s 00:00.0可以显示出Host Bridge:Memory Control Hub,该内存控制器负责总线地址的访问
附录:
对于提供了MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。
进程的4GB内存空间被人为的分为两个部分--用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB到4GB为内核空间,如下图:
内核空间中,从3G到vmalloc_start这段地址是连续物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如我们使用的VMware虚拟系统内存是1G,那么3G~3G+896M这片内存就应该映射物理内存,这部分虚拟内存对应实际内存0-896M,这部分虚拟内存与实际内存一一对应。在连续物理内存映射区之后,就是非连续物理内存映射区域,即vmalloc区域。对于1G的系统而言,vmalloc_start位置应在3G+896M附近(在物理内存映射区与vmalloc_start期间还存在一个8M的gap来防止跃界),vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射),如下图:
kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) extern inline unsigned long virt_to_phys(volatile void * address) { return __pa(address); } |
上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000)。
与之对应的函数为phys_to_virt(),将内核物理地址转化为虚拟地址:
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) extern inline void * phys_to_virt(unsigned long address) { return __va(address); } |
virt_to_phys()和phys_to_virt()都定义在include\asm-i386\io.h中。
而vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续
实例验证:验证get_free_page,kmalloc,vmalloc申请的虚存地址范围,前提:1G物理内存,high_mem = 896M
1 #include <linux/module.h> 2 #include <linux/gfp.h> //__get_free_page 3 #include <linux/slab.h> 4 #include <linux/vmalloc.h> 5 MODULE_LICENSE("GPL"); 6 unsigned long pagemem; 7 unsigned long kmallocmem; 8 unsigned long vmallocmem; 9 unsigned long funAddr; 10 void func(void) 11 { 12 ; 13 } 14 int __init mem_module_init(void) 15 { 16 unsigned long pG,pM; 17 unsigned long kG,kM; 18 unsigned long vG,vM; 19 unsigned long fG,fM; 20 pagemem = (unsigned long)__get_free_page(0); 21 pG = pagemem/(1024*1024*1024); 22 pM = (pagemem/(1024*1024))%1024; 23 printk("<1>pagemem addr=%luG,%luM\n", pG,pM); 24 25 kmallocmem = (unsigned long)kmalloc(100, 0); 26 kG = kmallocmem/(1024*1024*1024); 27 kM = (kmallocmem/(1024*1024))%1024; 28 printk("<1>kmallocmem addr=%luG,%luM\n", kG,kM); 29 30 vmallocmem = (unsigned long)vmalloc(1000000); 31 vG = vmallocmem/(1024*1024*1024); 32 vM = (vmallocmem/(1024*1024))%1024; 33 printk("<1>vmallocmem addr=%luG,%luM\n", vG,vM); 34 35 fG = ((unsigned long)func)/(1024*1024*1024); 36 fM = (((unsigned long)func)/(1024*1024))%1024; 37 printk("<1>moduleMem addr=%luG,%luM\n", fG,fM); 38 return 0; 39 } 40 41 void __exit mem_module_exit(void) 42 { 43 free_page(pagemem); 44 kfree((void*)kmallocmem); 45 vfree((void*)vmallocmem); 46 } 47 48 module_init(mem_module_init); 49 module_exit(mem_module_exit);
结果如下所示:
/*
root@ubuntu:/media/Kingston/mem# dmesg -c
[10437.830284] pagemem addr=3G,857M
[10437.830295] kmallocmem addr=3G,561M
[10437.830464] vmallocmem addr=3G,947M
[10437.830464] modulemem addr=3G,946M
*/
可见非连续物理内存映射区域(即3G+896M-4G)对应了vmalloc的分配和内核模块的内存分配