Linux 存储管理1——内存管理
1、CPU的页式管理
![[转载]Linux地址空间与地址映射资料汇总 [转载]Linux地址空间与地址映射资料汇总](http://blogimg.chinaunix.net/blog/upfile2/090512000453.png)
如上图,
1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。
2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的数据;
i386 CPU中的页式存储管理的思路是:通过页面目录和页面表分两个层次实现线性地址到物理地址的映射。这种映射模式在大多数情况下可以节省页面表所占用的空间。
Linux内核的设计要考虑到各种不同的CPU上的实现,还要考虑到64位的cpu上实现,所以不能仅针对i386结构来设计映射机制,而是要以一种假想的、虚拟的cpu和MMU(内存管理单元)为基础,设计出一种通用的模型,再把它落实到各种各样的cpu上。因此:linux的内核映射机制设计成三层,在页面目录和页面表中间增设了一层“中间目录”。在代码中页面目录称为;PGD、中间目录称为:PMD、而页面表称为:PT。PT中的表项称为:PTE(page table entry)。PGD/PMD/PT三者均为数组。
相应的,在逻辑上也把线性地址从高位到低位划分为成4个位段,各占若干位,分别作用于目录PGD中的下标,中间目录PMD中的下标、页面表中的下标以及物理页面内的位移。
具体来说,对于cpu发出的线性地址,虚拟的Linux内存管理单元分如下四步完成从线性地址到物理地址的映射:
1)用线性地址中最高的那一个位段作为下标在PGD中找到相应的表项,该表项指向相应的中间目录PMD。
2)用线性地址中的第二个位段作为下标在此PMD中找到对应的表项,该表项指向相应页面表。
3)用线性地址中的第三个位段作为下标在页面中找到对应的表项PTE,该表项中存放的就是指向物理页面的指针。
4)线性地址中最后位段为物理页面内的相对位移量,将此位移量与目标物理页面的起始地址相加便得到相应的物理地址。
从Pentium Pro开始,Intel引入了物理地址扩充功能PAE,允许将地址宽度从32位提高到36位,并且在硬件上支持三层映射模型,这样在Pentium Pro及以后的cpu上,只要将cpu的内存管理设置成PAE模式,就能是虚存的映射变成三层模式。
在32位系统中,每个指针大小为4个字节,所以,PGD表的大小为4*1024=4KB。而PMD是没有的,所以PMD的位段定义为0。
原理上来讲,Linux只需要为每个进程分配好所需数据结构,放到内存中,然后在调度进程的时候,切换寄存器cr3,剩下的就交给硬件来完成了(呵呵,事实上要复杂得多,不过偶只分析最基本的流程)。
前面说了i386的二级页管理架构,不过有些CPU,还有三级,甚至四级架构,Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为:
页全局目录PGD(对应刚才的页目录)
页上级目录PUD(新引进的)
页中间目录PMD(也就新引进的)
页表PT(对应刚才的页表)。
整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图:
![[转载]Linux地址空间与地址映射资料汇总 [转载]Linux地址空间与地址映射资料汇总](http://blogimg.chinaunix.net/blog/upfile2/090512000528.png)
那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?嗯,来看这种情况下,怎么来划分线性地址吧!
从硬件的角度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。
从软件的角度,由于多引入了两部份,,也就是说,共有五部份。——要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将页上级目录和页中间目录的长度设置为0就可以了。
这样,操作系统见到的是五部份,硬件还是按它死板的三部份划分,也不会出错,也就是说大家共建了和谐计算机系统。
这样,虽说是多此一举,但是考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了,这样,软件与硬件再次和谐——抽像就是强大呀!!!
例如,一个逻辑地址已经被转换成了线性地址,0x08147258,换成二制进,也就是:
0000100000 0101000111 001001011000
内核对这个地址进行划分
PGD = 0000100000
PUD = 0
PMD =
PT = 0101000111
offset = 001001011000
现在来理解Linux针对硬件的花招,因为硬件根本看不到所谓PUD,PMD,所以,本质上要求PGD索引,直接就对应了PT的地址。而不是再到PUD和
从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,再到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。
然后交给硬件,硬件对这个地址进行划分,看到的是:
页目录
PT = 0101000111
offset = 001001011000
嗯,先根据0000100000(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接着,再加一个offset,就是最终的物理地址了。
linux的高端内存映射方式:
注:
可以看到__GFP_HIGHMEM就是表示先考虑高端内存;
参考资料:
http://os.51cto.com/art/201205/333728.htm
我们可以用Linux的实用程序objdump对你的程序进行反汇编,从而知晓其地址范围。
例如:假定我们有一个简单的C程序Hello.c
# include <stdio.h>
greeting ( )
{
printf(“Hello,world!\n”);
}
main()
{
greeting();
}
之所以把这样简单的程序写成两个函数,是为了说明指令的转移过程。我们用gcc和ld对其进行编译和连接,得到可执行代码hello。然后,用Linux的实用程序objdump对其进行反汇编:
$objdump –d hello
得到的主要片段为:
08048568 <greeting>:
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 <main>:
804857c: pushl %ebp
804857d: movl %esp, %ebp
804857f: call 8048568 <greeting>
8048584: leave
8048585: ret
8048586: nop
8048587: nop
其中,像08048568这样的地址,就是我们常说的虚地址(这个地址实实在在的存在,只不过因为物理地址的存在,显得它是“虚”的罢了)。
Linux虚拟内存的大小为2^32=4G(在32位的x86机器上),内核将这4G字节的空间分为两部分。最高的1G字节(从虚地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。
每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间则为所有进程以及内核所共享。另外,进程的“用户空间”也叫“地址空间”,在后面的叙述中,我们对这两个术语不再区分。
用户空间不是进程共享的,而是进程隔离的。每个进程最大都可以有3GB的用户空间。一个进程对其中一个地址的访问,与其它进程对于同一地址的访问绝不冲突。比如,一个进程从其用户空间的地址0x1234ABCD处可以读出整数8,而另外一个进程从其用户空间的地址0x1234ABCD处可以读出整数20,这取决于进程自身的逻辑。
任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个4GB的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。由此可以看出,每个进程都有自己的虚拟地址空间,只有此进程运行的时候,其虚拟地址空间才被运行它的CPU所知。在其它时刻,其虚拟地址空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4 GB的虚拟地址空间,但在CPU眼中,只有一个虚拟地址空间存在。虚拟地址空间的变化,随着进程切换而变化。
从上面我们知道,一个程序编译连接后形成的地址空间是一个虚拟地址空间,但是程序最终还是要运行在物理内存中。因此,应用程序所给出的任何虚地址最终必须被转化为物理地址,所以,虚拟地址空间必须被映射到物理内存空间中,这个映射关系需要通过硬件体系结构所规定的数据结构来建立。这就是我们所说的段描述符表和页表,Linux主要通过页表来进行映射。
于是,我们得出一个结论,如果给出的页表不同,那么CPU将某一虚拟地址空间中的地址转化成的物理地址就会不同。所以我们为每一个进程都建立其页表,将每个进程的虚拟地址空间根据自己的需要映射到物理地址空间上。既然某一时刻在某一CPU上只能有一个进程在运行,那么当进程发生切换的时候,将页表也更换为相应进程的页表,这就可以实现每个进程都有自己的虚拟地址空间而互不影响。所以,在任意时刻,对于一个CPU来说,只需要有当前进程的页表,就可以实现其虚拟地址到物理地址的转化。
4、物理地址、逻辑地址、虚拟地址、线性地址
地址转换
linux的内存地址空间
|
用户空间(0x0~0xBFFFFFFF) |
用户空间(0x40000000~0xFFFFFFF) |
系统空间(0x0~0x3FFFFFFF) |
|
|
|
|
物理区(896M) |
5、内核空间到物理内存的映射
内核空间对所有的进程都是共享的,其中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据,不管是内核程序还是用户程序,它们被编译和连接以后,所形成的指令和符号地址都是虚地址(参见2.5节中的例子),而不是物理内存中的物理地址。
虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始的,如图4.2所示,之所以这么规定,是为了在内核空间与物理内存之间建立简单的线性映射关系。其中,3GB(0xC0000000)就是物理地址与虚拟地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
我们来看一下在include/asm/i386/page.h头文件中对内核空间中地址映射的说明及定义:
#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()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。