本文主要以32位机器为准。
1.0 虚拟地址、物理地址、逻辑地址、线性地址
虚拟地址又叫线性地址。linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)(在用户态,内核态逻辑地址专指下文说的线性偏移前的地址)是一个概念。物理地址则是内存的实际地址。
内核的虚拟地址和物理地址,大部分只差一个线性偏移量。用户空间的虚拟地址和物理地址则采用了多级页表进行映射,但仍称之为线性地址。
1.1 虚拟内存的作用:
1.扩展实际有限的物理内存,当然这种扩展是虚拟的,比如物理内存512M,对于一个需要1G空间的进程来说,照样可以运行。
2. 使得进程中的数据空间增大,增大到多少与硬件有关,对于一个32位的芯片,进程中的数据空间可以为4G[2^32],对于64位的芯片则支持2^64大小 的空间。这一点使得进程自身可操作的空间大大增加。
通俗来讲,虚拟内存的管理的核心是解决如何在小的物理内存中运行更大程序的问题。
在Linux中,解决这个问题的关键是一个叫做pagetable[PT页面转换表]的结构。Linux把物理内存分为了固定统一大小的块,称为page[页],一般为4KB,并且每个页都有一个编号 [page frame number]。这样一个512M大小的内存将包括128K个页。这种方式称为paging,使得操作系统对内存的管理更方便。pagetable的作用就是将进程操作的地址[虚拟地址]转换成物理地址。
其原理很简单,如下:
用一个32位芯片的系统为例[64位同理],运行的每个进程的可操作数据空间为2^32,即2^20个页,设其物理内存为512M,则物理页有2^17个,现在就说明如何将2^20个页放入2^17个页中运行。我们把进程操作的地址分为两部分,第一部分为地址的高20位,第二部分为后12位,这样很容易将第一部分理解为虚拟页标号,第二部分理解为在页中的offset。那么现在我们只需将虚拟页标号对应到物理页号即可,这个对应就是page table的工作,在这个例子中pagetable包括了2^20个记录,每个记录有两部分组成:20位的虚拟标号和17位的物理标号,这样CPU用进程地址的第一部分作为索引找到对应的17位物理标号,与地址的第二部分一起便组成一个29位的地址,这个地址就是要找的物理地址。因为物理页少于虚拟页,所以pagetable中的有些记录的后17位是空的或无效的。
利用这个方法,使得运行的进程无需知道自己操作的地址是虚拟的,和运行在一个真实的大物理内存中效果是一样的。
可以看出,在进程的运行过程中,page table必须一直保存在内存中,在上面的例子中,我们把虚拟地址分了2层,pagetable有2^20个记录,需要1M左右的空间,为了节省空间我们可以将地址分为3层,第一层10位,需要1K左右的空间,第二层10位,需要1K左右的空间,第三层12位,这样在一段时间内只需要2K的空间保存pagetable。实际上,Alpha的芯片采用的就是这种3层的分法,Intel的芯片采用的2层的分法。
1.2 地址空间分布由于linux下不主张将程序分段,而是分页,所以即使是在80x86的体系结构下,段的基地址也是0.因此逻辑地址、线性地址、虚拟地址在linux中都是相同的。所以对于linux下的elf文件来说,代码段的起始地址0x08048000即使逻辑地址也是虚拟地址。
1、X86物理地址布局
[ 4G |--| 3G |--| 896M |-------------| 16M |---| 1M |---| 0M ]
|<ZONE_HIGHMEM>|<ZONE_NORMAL>|<ZONE_DMA>|
linux系统在初始化时,会根据实际的物理内存的大小,为每一个物理页面创建一个page对象,所有的page对象构成一个mem_map数组。
ZONE_DMA的范围是0-16M,该区域的物理页面专门供I/O设备的DMA使用。因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。
ZONE_NORMAL 的范围为16M-896M,该区域的物理页面时内核可以以直接使用的,一一映射,有个偏移地址。
ZONE_HIGHMEM 的范围是896-结束,改区域为高端内存,内核不能直接使用,多通过多级页表进行映射。
2、linux虚拟地址内核空间分布
[ 4G | fix|kmap|vmalloc | 3G +896M |--| mem_map | kernel Image | 3G+16M |---| 3G |------------| 0M ]
|<---------high_memory=128M---->|
|<----------------------kernel space -------------------------------------------------->|<---user space---->|
在kernel image下面有16M的内核空间用于DMA操作。位于内核空间高端的128M地址主要由3部分组成,分别为vmalloc area,持久化内核映射区,临时内核映射区。由于ZONE_NORMAL和内核线性空间存在直接映射关系,所以内核会将频繁使用的数据如kernel代码、GDT、mem_map舒卓等放在这个区域。二将用户数据、页表(PT)等不常用数据放在ZONE_HIGHMEM里,只在要访问这些数据时才建立映射关系(kmap)。比如,但内核要访问I/O设备存储空间时,就使用ioreamap将位于物理地址高端的mmio区内存映射到内核空间的vmalloc area中,在使用完之后便断开映射关系。
3、虚拟地址用户空间分布
[ 4G | 3G+896M | 3G | env/param|stack|--|---------|mmap |---| heap | bss | data | text | 0x08048000 | --|0M]
|<--kernel space---->|<-------------------------------------user space-------------------------------------------->|
用户进程的代码区一般从虚拟地址空间的0x08048000开始,这是为了便于检查空指针。代码区之上便是数据区,未初始化数据区,堆区,栈区,以及参数、全局环境变量。
一段代码实例,说明用户空间分布:
b.c 代码如下:
#include<stdio.h>
int global_init_var= 84;
intglobal_uninit_var;
void func(int i)
{
printf("zll-----n=%d\n",i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func(static_var + static_var2 + a + b);
printf("global_init_var=%p,global_uninit_var=%p",&global_init_var,&global_uninit_var);
printf("static_var=%p,static_var2=%p",&static_var,&static_var2);
printf("a=%p,b=%p",&a,&b);
return 0;
}
执行命令:
(1) gcc-o b.out b.c
(2)./b.out
输出:global_init_var=0x601040,global_uninit_var=0x601050static_var=0x601044,static_var2=0x60104ca=0x7ffff0602578,b=0x7ffff060257c
(3)readefs-S b.out
输出如下(得到段的地址):
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8 0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 00000318 000000000000003f 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400358 00000358 0000000000000008 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400360 00000360 0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400380 00000380 0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400398 00000398 0000000000000048 0000000000000018 A 5 12 8
[11] .init PROGBITS 00000000004003e0 000003e0 000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400400 00000400 0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400440 00000440 00000000000001c2 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000400604 00000604 0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000400610 00000610 0000000000000012 0000000000000000 A 0 0 4
[16] .eh_frame_hdr PROGBITS 0000000000400624 00000624 000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400660 00000660 0000000000000114 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600e10 00000e10 0000000000000008 0000000000000000 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600e18 00000e18 0000000000000008 0000000000000000 WA 0 0 8
[20] .jcr PROGBITS 0000000000600e20 00000e20 0000000000000008 0000000000000000 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000600e28 00000e28 00000000000001d0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000600ff8 00000ff8 0000000000000008 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000601000 00001000 0000000000000030 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000601030 00001030 0000000000000018 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000601048 00001048 0000000000000010 0000000000000000 WA 0 0 4
[26] .comment PROGBITS 0000000000000000 00001048 0000000000000024 0000000000000001 MS 0 0 1
[27] .shstrtab STRTAB 0000000000000000 0000106c 0000000000000108 0000000000000000 0 0 1
[28] .symtab SYMTAB 0000000000000000 000018f8 0000000000000690 0000000000000018 29 47 8
[29] .strtab STRTAB 0000000000000000 00001f88 000000000000027d 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge),S (strings), l (large)
I (info), L (link order), G (group), T (TLS),E (exclude), x (unknown)
O (extra OS processing required) o (OSspecific), p (processor specific)
对比输出结果:
global_init_var=0x601040--------------------全局赋值变量位于data区
global_uninit_var=0x601050-------------------全局不赋值变量位于bss区
static_var=0x601044-------------------------------局部静态赋值变量位于data区
static_var2=0x60104c------------------------------局部静态不赋值变量位于bss区
a=0x7ffff0602578------------------------------------局部赋值变量位于栈区
b=0x7ffff060257c------------------------------------局部不赋值变量位于栈区
1.3 总结
1、32位地址空间,通过2^32算得有4G空间,其中0~3G为用户空间,3G~4G为内核空间。
2、物理地址,为实际内存的地址空间,如1G、2G、3G、4G等,所以分配为0~16M为DMA区,16~896M为NORMAL区,896~最大为HIGH区
3、对于线性地址,[0-3G)为用户空间,其对物理地址的映射随进程不同而变化; 用户空间常见的内存分配方式为malloc函数;
4、对线性地址[3G-4G)内核空间,其对物理地址的映射关系是由内核负责的,不会随进程改变,是固定的。 内核空间内存分配方式 有kmalloc,vmalloc,kmap;
5、内核空间地址分为直接内存映射区、动态内存映射区、永久内存映射区、固定内存映射区。直接映射区映射的物理地址是低端内存(小于896M的范围)。动态映射区可映射为物理地址的低端内存或高端内存,永久映射区映射物理地址的高端内存。(物理地址大于896M的内存称为高端内存。)固定映射区的每个地址都用于特殊的用途。
6、之所以引入内核空间对高端内存的特定的一些映射方式,是使内核可以实现对物理内存的全部访问。直接映射建立了对896M以下物理内存的映射。