Linux-虚拟地址、物理地址、IO内存、IO端口、映射、内核空间、用户空间、IO空间、内存空间关系 转载请注明出处:http://blog.csdn.net/guolele2010 作者:guolele 这题目够长的,也是笔者想一次性弄好,现在开始吧! 讲基础知识之前,我们先了解一下一个编译的二进制程序的内容,这里引用《解惑—Linux中的地址空间》例如:假定我们有一个简单的C程序Hello.c
[cpp] view plaincopyprint?
- # include
- greeting ( )
- {
- printf(“Hello,world!/n”);
- }
- main()
- {
- greeting();
- }
之所以把这样简单的程序写成两个函数,是为了说明指令的转移过程。我们用gcc和ld对其进行编译和连接,得到可执行代码hello。然后,用Linux的实用程序objdump对其进行反汇编:
$objdump –d hello
得到的主要片段为:
08048568 :
8048568: pushl %ebp
8048569: movl %esp, %ebp
804856b: pushl $0x809404
8048570: call 8048474 <_init+0x84>
8048575: addl $0x4, %esp
8048578: leave
8048579: ret
804857a: movl %esi, %esi
0804857c :
804857c: pushl %ebp
804857d: movl %esp, %ebp
804857f: call 8048568
8048584: leave
8048585: ret
8048586: nop
8048587: nop
08048568这里的这个地址是虚拟地址,但大家都知道,真正的代码啊什么,是放在外设物理设备上,物理设备就有个叫物理地址。这是个应用程序,但如果是内核模块呢?那又涉及到内核地址,别蒙!慢慢来。 虚拟地址,物理地址,内核空间,用户空间,内核逻辑地址在32位地址总线的CPU里,linux支持的虚拟内存大小为2^32=4G,其中又将这4G分为两部分。最高的1G字(从虚拟地址0xc0000000~0xffffffff)是内核的虚拟地址空间,称为“内核空间”,剩下的3G(0x0~0xbfffffff)为“用户空间”。因为用户空间又可以通过系统调用来进入内核空间,因此,内核空间是所有用户空间进程共享的,所以,从进程角度看,每个进程拥有4G的虚拟地址空间(也叫虚拟内存)。 每个进程都拥有3G的虚拟空间,这个空间又对其它进程是不可见的,只有内核空间是共享的,进程的“用户空间”也叫“地址空间”。进程是隔离的,每个进程访问同一个地址(这里指虚拟地址),得到的数值可以是不一样的,如进程1在0x1读到1,进程2可以在0x1读到2,这就取决于它对应的数据存储的物理地址不一样。物理地址就是实实在在,地址总线直接取得的。 有一点特别注意,CPU只用虚拟地址,然后虚拟地址经过硬件MMU直接解映射为物理地址再送到总线上,然后再操作外设。 CPU任一时刻只能有一个进程占用,那么对CPU来说,整个系统就只有一个4GB的虚拟地址空间,那切换进程时,虚拟空间也要跟着切换,因为每个进程都是进程隔离的,所以其中的3G就要地址切换,那么是如何切换的呢?答案在于段式管理与页式管理,其中linux是采用页式管理的。物理地址通过页式和段式管理(linux只有页式,后面只说页式),用户空间把物理地址转变成虚拟地址,内核空间是简单的线性映射关系(没涉及到页式管理)。而把虚拟地址链接到物理地址这个操作又叫做映射。进程具体操作就是CPU只需要切换页表就可以实现进程虚拟地址映射逻辑的变化了。 内核空间与物理内存的转换 内核空间与物理内存的转换也是内核虚拟地址与物理地址的转换。内核空间对所有的进程都是共享的,其中存放的是内核代码和数据、堆栈、BSS等,而用户空间的就是用户空间的代码、数据、堆栈、BSS等。所有程序无论是在内核还是用户空间的,最终转换成的地址都是虚拟地址。虽然内核空间占据了虚拟空间中的最高1G字节,但映射到物理内存中却是从最低地址(0x0)开始,因为内核空间与物理内存之间建立的映射关系是简单的线性映射,就是移了一个位移量。其中3GB(0xc0000000)就是物理地址和虚拟地址之间的位移量,在linux里叫做PAGE_OFFSET。 #define __PAGE_OFFSET (0xC0000000)
……
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
对于内核空间而言,给定一个虚地址x,其物理地址为“x- PAGE_OFFSET”,给定一个物理地址x,其虚地址为“x+ PAGE_OFFSET”。
这里再次说明,宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。
这图描述的是内核空间的分布(更具体看下面内核memory.txt里的,这里的有点不太对),其中 PAGE_OFFSET是上面说的线性映射的位移量 High_memory是说高端内存,通常,我们把物理地址(物理内存)超过896M的区域称为高端内存。也就是说,MEM>896M,high_memory = 0XC0000000 + MEM – 896M,其中mem为所有的物理地址空间大小。一般少于896M就是0xc0000000 + mem。 在源代码(2.6.26)中函数mem_init中, max_mapnr = virt_to_page(high_memory) - mem_map; 先获得物理地址再映射虚拟地址,虚拟地址再到页 #define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT) 虚拟地址转换到物理地址,只要线性偏移一下就可以,其中PYHYS_OFFSET是物理内存的偏移,即比0x0地址多多少,简单的线性映射 #define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET) 就是物理地址映射到虚拟地址 __pa(kaddr) >> PAGE_SHIFT 下面这个函数的决定要看内核配置.config,这里是CONFIG_FLATMEM,所以调用这个,得到虚拟地址的页表 #define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET)) 所以在图中,PAGE_OFFSET到high_memory 之间就是所谓的物理内存映射。只有这一段之间,物理地址与虚地址之间是简单的线性关系。 还要说明的是,要在这段内存分配内存,则调用kmalloc()函数。反过来说,通过kmalloc()分配的内存,其物理页是连续的。 下面是内核关于memory安排的描述 Start End Use -------------------------------------------------------------------------- ffff8000 ffffffff copy_user_page / clear_user_page use. For SA11xx and Xscale, this is used to setup a minicache mapping. ffff1000 ffff7fff Reserved. Platforms must not use this address range. ffff0000 ffff0fff CPU vector page. The CPU vectors are mapped here if the CPU supports vector relocation (control register V bit.) ffc00000 fffeffff DMA memory mapping region. Memory returned by the dma_alloc_xxx functions will be dynamically mapped here.DMA内存映射区域 ff000000 ffbfffff Reserved for future expansion of DMA mapping region. VMALLOC_END feffffff Free for platform use, recommended. VMALLOC_END must be aligned to a 2MB boundary. VMALLOC_START VMALLOC_END-1 vmalloc() / ioremap() space. Memory returned by vmalloc/ioremap will be dynamically placed in this region. VMALLOC_START may be based upon the value of the high_memory variable. PAGE_OFFSET high_memory-1 Kernel direct-mapped RAM region. 内核直接映射内存区域 This maps the platforms RAM, and typically maps all platform RAM in a 1:1 relationship. TASK_SIZE PAGE_OFFSET-1 Kernel module space内核模块空间 Kernel modules inserted via insmod are placed here using dynamic mappings. 00001000 TASK_SIZE-1 User space mappings用户空间 Per-thread mappings are placed here via the mmap() system call. 00000000 00000fff CPU vector page / null pointer trap CPUs which do not support vector remapping place their vector page here. NULL pointer dereferences by both the kernel and user space are also caught via this mapping. 非连续区的分配调用VMalloc()函数。 vmalloc()与 kmalloc()都是在内核代码中用来分配内存的函数,但二者有何区别? 从前面的介绍已经看出,这两个函数所分配的内存都处于内核空间,即从3GB~4GB;但位置不同,kmalloc()分配的内存处于3GB~high_memory之间,这一段内核空间与物理内存的映射一一对应,而vmalloc()分配的内存在VMALLOC_START~4GB之间,这一段非连续内存区映射到物理内存也可能是非连续的。 vmalloc()工作方式与kmalloc()类似, 其主要差别在于前者分配的物理地址无需连续,而后者确保页在物理上是连续的(虚地址自然也是连续的)。 尽管仅仅在某些情况下才需要物理上连续的内存块,但是,很多内核代码都调用kmalloc(),而不是用vmalloc()获得内存。这主要是出于性能的考虑。vmalloc()函数为了把物理上不连续的页面转换为虚拟地址空间上连续的页,必须专门建立页表项。还有,通过vmalloc()获得的页必须一个一个的进行映射(因为它们物理上不是连续的),这就会导致比直接内存映射大得多的缓冲区刷新。因为这些原因,vmalloc()仅在绝对必要时才会使用——典型的就是为了获得大块内存时。 上面在讲linux虚拟地址分布时,扯到一个VMALLOC_START VMALLOC_END-1这个区域,用于vmalloc和ioremap,对于ioremap,又要引入几个概念。 IO内存、IO端口、IO空间
看上面两图应该能大概明白,为什么有两个,第一个图目的是告诉你,外设上的寄存器或者内存连接在io空间或者内存空间是由硬件决定的,第二个图是告诉你并不只是寄存器能当端口,内存也可以是IO端口。 IO端口:当一寄存器或内存位于IO空间时,称为IO端口 IO内存:当一寄存器或内存位于内存空间时,称为IO内存 独立编址和统一编址 讲到IO端口和IO内存,其实跟独立编址和统一编址一样,因为有些CPU是IO空间与内存空间都有,如果X86,那它把Io端口独立开来,所以就叫独立编址,而一些设备如果ARM,只有内存空间,所以只能将所以都统编址。 讲完这些,就要讲IO端口IO内存与物理地址、虚拟地址之间的关系(linux)。 首先在linux中,IO端口与IO内存统一为IO资源,在linux里有两个文件显示IO端口跟IO内存,分别是/proc/ioport与/proc/iomem,这两个其实对于实际的硬件驱动作用不大,它只是在用户空间表现出来。下面分别介绍一下。 IO端口: 操作步骤:1、申请 2、访问 3、释放 1、申请 Request_region()这其实是在加入内核描述io端口的链表中,将在用的设备显示在/proc/ioport里 2、访问 inb和outb:
在Linux设备驱动中,宜使用Linux内核提供的函数来访问定位于I/O空间的端口,这些函数包括:
· 读写字节端口(8位宽)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
· 读写字端口(16位宽)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
· 读写长字端口(32位宽)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
· 读写一串字节
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
· insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()将addr指向的内存的count个字节连续地写入port开始的端口。
· 读写一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
· 读写一串长字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
上述各函数中I/O端口号port的类型高度依赖于具体的硬件平台,因此,只是写出了unsigned。 3、释放 release_region() check_region()检查
IO内存: 1、申请 申请用request_mem_region()显示在/proc/iomem里,这步只是告诉别人,我已经用了这段内存,你别用了。不是必要的。 2、映射 Ioremap() 非常重要,因为CPU操作的是虚拟地址,如果你直接给物理地址它,它就发疯了,所以你应该先将物理地址映射成虚拟地址,这步得到的地址就是可以操作的地址。 3、访问 读I/O内存
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
· 写I/O内存
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 4、释放 Release_mem_region() 把I/O端口映射到“内存空间”:
void *ioport_map(unsigned long port, unsigned int count);
通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。当不再需要这种映射时,需要调用下面的函数来撤消:
void ioport_unmap(void *addr);
实际上,分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口访问I/O端口。
|