内存管理第一谈:段式管理和页式管理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jy1075518049/article/details/43610569
 对于内存管理这个操作系统中庞大的体系,实在是容易让人望而止步,市面上介绍这块知识的书籍其实很多,但是由于书面语言的缘故,总感觉有些东西晦涩难懂,先后看过的书籍有《操作系统基本原理》、《linux内核完全注释》、《深入理解linux内核》、《linux内核源代码情景分析》,不过依然只是明白了一个大体的框架,对于具体的一些细节,还是迷迷糊糊的。下面内容只是想把一些书面语言转换为比较容易理解的内容,希望能对其他人有所帮助吧。

一:内存寻址(段式管理)
1.逻辑地址、线性地址、物理地址
很长一段时间,我都搞不懂这三个地址到底是怎么回事,因为中间牵扯着地址转换,各种寄存器,各种描述符,描述表,每次看书看着看着就彻底晕菜了。
物理地址:最容易理解的,它就是实实在在物理内存上的地址,你PC上有1G内存,那最大地址就是0x40000000,0x800就是代表1KB的地址。
线性地址:这是APP用的地址,也就是我们程序员写代码用的地址,它是一个虚拟地址,最终会被转化到物理地址。
逻辑地址:这是最麻烦的一个地址了,它是针对x86的架构形成的地址,你可以先不用理会,明白上面两个就行了

从线性地址说起,早期的x86的CPU内部有20根地址线,能寻址2^20个地址(这些就是全部的线性地址),也就是1MB,但是其中的寄存器只有16位,只能寻址 2^16个地址,也就是64KB,这就是带来个问题,怎么利用寄存器的16位来寻址1MB呢?于是intel想出了一个办法,把寻址分为两部分,基地址和偏移地址,这也我们以前经常接触的代码段、数据段的由来,实际上就是带表的基地址。那么,CPU要访问一个20位地址,比如这个地址是代码段地址,那么首先CPU要到CS(代码段寄存器)中取出基地址,然后再到IP寄存器中取出指令偏移量,组合成一个20位地址(这就是线性地址)然后寻址。

这就又带来一个问题,你说寄存器都是16位的,那么相加能表示的最大地址就是 0xffff+0xffff=128KB,还是不够1MB啊,怎么办呢?为了解决这个问题,cs寄存器中的数字不再代表实际基地址,而是基地址一个索引。我们把1MB的内存分成29个64KB(偏移寄存器的最大访问量),然后在cs寄存器中存放1-29这30个数字,这样就能访问到任意一个1MB内的地址了。现在的 线性地址 = cs*64KB + IP
举个例子:假如cs存的索引值是5,IP存的是0x556,那么实际CPU得到的线性地址就是 5*0x10000+0x556 = 0x50556 = 321.333KB,就这么简单

理解了上面问题,那么逻辑地址就很容易理解了,其实就是cs:ip的一种组合地址,你可以理解为一个cpu用的中间地址,它就是段寄存器和偏移寄存器的一个组合,在没有经过上述处理前没有任何意义。下图说明这种关系:

图上又多了一些标志,这是现在操作系统(32位)的一种转换方式,选择符就是对应上面的CS寄存器,偏移量就是对应上面的IP寄存器,只是它嫌32位的寄存器能表达信息太少了,于是不再直接由上面方式得出线性地址,而是先把段基址存到一块内存,每个段基址占8字节,这8字节不仅有段基址,还有很多描述段的信息。所有描述符会把index字段*8得到某个段的描述符的地址,然后跑到那个地址取出里边的段基址再加上IP寄存器的偏移量就得到线性地址了。gdtr这个地方跳过就行。如今段式管理不理解也无所谓,本质还是上面说的东西。

那么有人就问了,线性地址和物理地址又存在什么关系呢?你可以想象这么一种场景,还是这20位的cpu,上面运行着qq,现在你发了一条消息,但这条消息的指令在 0x50556 地址处,上面讲了,这时候cs=5,ip=0x556,好了,cpu知道要去 0x50556地址取东西了,结果呢,你计算机只有 64KB 的物理内存,CPU一看根本没有这个地址,程序就完蛋了。解决这个问题有两种方案,第一,浪费CPU的4跟地址线,规定系统支持最大内存就是64KB。第二,想其它办法。

很明显,第一种完全不可取,为了解决这个问题,就有了内存管理中的分页管理机制,用来确定线性地址和物理地址之间的关系。

二:页式管理机制
如果做linux下内核开发,对于上述的x86的段式管理可以完全不用理会,因为linux根本没有用intel弄出来的这个段式管理,而是以页式管理完成了所有的内存管理工作。在说这块内容之前先做一个小实验来引入页式管理机制。

试验内容:同一个程序,运行两次(产生两个进程),然后观察进程运行空间
这是程序源码,很简单,为了不至于瞬间结束,睡眠100s
/* test.c */
#include <stdio.h>

int main()
{
	int i = 0;
	i = i + 1;

	sleep(100);
}


编译:gcc -o test test.c

在后台运行两次:
jy@ubuntu:/ceshi/app/test$ ./test&
[1] 18368
jy@ubuntu:/ceshi/app/test$ ./test&
[2] 18369

用PS命令查看进程号:
jy@ubuntu:/ceshi/app/test$ ps
  PID TTY          TIME CMD
17751 pts/2    00:00:00 bash
18368 pts/2    00:00:00 test
18369 pts/2    00:00:00 test
18370 pts/2    00:00:00 ps

可以看到两个进程的进程号分别是 18368 和 18369,然后到proc文件系统查看进程地址空间
jy@ubuntu:/ceshi/app/test$ cat /proc/18368/maps 
08048000-08049000 r-xp 00000000 08:01 1049688    /ceshi/app/test/test
08049000-0804a000 r--p 00000000 08:01 1049688    /ceshi/app/test/test
0804a000-0804b000 rw-p 00001000 08:01 1049688    /ceshi/app/test/test
b7607000-b7608000 rw-p 00000000 00:00 0 
b7608000-b77a7000 r-xp 00000000 08:01 1049503    /lib/i386-linux-gnu/libc-2.15.so  //sleep函数的动态链接库运行地址
b77a7000-b77a9000 r--p 0019f000 08:01 1049503    /lib/i386-linux-gnu/libc-2.15.so
b77a9000-b77aa000 rw-p 001a1000 08:01 1049503    /lib/i386-linux-gnu/libc-2.15.so

jy@ubuntu:/ceshi/app/test$ cat /proc/18369/maps 
08048000-08049000 r-xp 00000000 08:01 1049688    /ceshi/app/test/test
08049000-0804a000 r--p 00000000 08:01 1049688    /ceshi/app/test/test
0804a000-0804b000 rw-p 00001000 08:01 1049688    /ceshi/app/test/test
b754f000-b7550000 rw-p 00000000 00:00 0 
b7550000-b76ef000 r-xp 00000000 08:01 1049503    /lib/i386-linux-gnu/libc-2.15.so
b76ef000-b76f1000 r--p 0019f000 08:01 1049503    /lib/i386-linux-gnu/libc-2.15.so
b76f1000-b76f2000 rw-p 001a1000 08:01 1049503    /lib/i386-linux-gnu/libc-2.15.so

发现没有,两个进程的地址空间居然全部一样,而不同程序是不能运行在同一块物理内存地址的,否则会发生混乱。这就说明,我们看到的这个进程地址根本就不是真正的物理内存地址,而是所谓的虚拟地址,也就是线性地址。可想而知,linux为两个进程分配了相同的线性地址,而通过页式管理机制把相同的线性地址映射到了不用的物理内存块,从而实现了内存管理。

PS:我记得以前在哪个地方看到过windows下的相关实验,结论好像是一样的,没有linux平台的可以查一下怎么在win上实现相关实验

好了,你现在明白基本原理了吧,下面来看下真正的实现机制。
在这之前先约定几个名词,这是为了大家以后在阅读相关书籍时候不会晕菜
1.页目录、页表:页式管理的两个线性表,用来查找存线性地址的页
2.页框:对应真正的物理地址,以4KB为边界

好吧,我承认上面解释大部分人还是看不懂,不要着急,你先有个概念,就是1其实就是一张张的表(它的作用就是查表找到某个线性地址对应的物理地址),2代表是物理地址。等读完了后边或许那就明白啥意思了。

下面结合一副图来说明页式管理机制,这两个图都出自《深入了解linux内核》

 
图上线性地址就是经过第一部分段式管理转换得到的地址,如果你跳过上面第一部分的话,完全可以理解为就是我们平时程序运行的地址,这些地址是你能通过某些命令获得的,真正的物理地址内核是不会让你看到的,否则人人不都能变成黑客了。言归正传,现在CPU知道了这个32位的线性地址,那么如何到真正的物理内存取出需要的数据呢?
看下图,这幅图是我画的,所以比较丑,凑合看吧
图片 
首先,这32位的线性地址会被分成3个部分,10位页目录,10位页表,12位页偏移。好了,又回来了,页目录和页表,你可以理解为内存中存储的一张张的拥有1024个表项的二维表,页目录每一项存放的是某个页表的地址,而页表中每一项存放的是某个页的地址。你可以理解为C语言里的指针,每个表项存的都是一个指针,指向某个内存地址。这个内容是必须要理解的,我给大家举个具体的例子:
假如线性地址是 0x00401001 ,表示为二进制是 0000000001 0000000001 000000000001 ,对应上面三个名词,那么页目录值、页表值和页内偏移值都是1,也就说要想找到真正的物理地址,首先应该到页目录(拥有1024个页目录项)的第一项中寻址,这个项中存的是页表的地址,假如是1000,CPU就会跑到1000的物理地址(注意,真的是物理地址)上找到某个页表,这个页表又拥有1024项的页表项。然后拿到第二个10位地址(还是1),也就说到第一个页表上的第一项,到这里,其实就已经能找到物理内存块了。想一下,每个页表项代表4KB,每个页表又1024个页表项,而页目录项总共有1024个页目录项(=1024张页表),所以能表示的总大小正好是
4kb*1024*1024=4GB。而上面所说最终找到的这个页表项里边存的就是一个指针(你可以这么想象,其实不是),它指向一块真实物理内存的边界。并不是每一个页表项都有物理内存相对应的,内核只为进程用到的那些线性地址实时的分配物理内存页,这种叫写时复制机制,这里简单一提,等到后边再介绍。好了,通过两张表的查找,终于通过线性地址的高20位找到了对应的4KB物理内存,那么怎么找到某个字节呢,相信都想到了,就是最后的12位作用了,找到了这4KB的物理内存边界,再加上12位的偏移值即可。12能表示多大呢?2^12=4096,正好是4KB,所以就能精确找到4KB区域内的任意一个字节,设计的巧妙吧。

图中蓝线就是上面寻址过程。页管理机制吧,说实话挺抽象的,需要多看几遍,自己画画图才能真正理解,上面说的这些可能也有不当地方,毕竟细节处我也没有去追踪代码,不过大部分还是对的。想要真正彻底搞懂,还是看源码吧,《linux内核完全注释》这一块讲的还是蛮不错的,想深入了解的可以先看下这本书。

内存管理其他部分,请期待第二谈...

没有更多推荐了,返回首页