分页式存储管理
1、虚拟地址和页表的由来
思考一下,如果在没有虚拟内存和分页机制的情况下,每一个用户程序在物理内存上所对应的空间必须是连续的,如下图:
因为每一个程序的代码、数据长度都是不一样的,按照这样的映射方式,物理内存将会被分割成各种离散的、大小不同的块。经过一段运行时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎片的形式存在。
怎么办呢?我们希望操作系统提供给用户的空间必须是连续的,但是物理内存最好不要连续。 此时虚拟内存和分页便出现了,如下图所示:
把物理内存按照一个固定的长度的页框进行分割,有时叫做物理页。每个页框包含一个物理页 (page) 。一个页的大小等于页框的大小。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。区分一页和一个页框是很重要的:
- 页框是一个存储区域;
- 页是一个数据块,可以存放在任何页框或磁盘中。
有了这种机制,CPU便并非是直接访问物理内存地址,而是通过虚拟地址空间来间接的访问物理内存地址。所谓的虚拟地址空间,是操作系统为每一个正在执行的进程分配的一个逻辑地址,在32位机上,其范围从0 ~ 4G-1。
操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系,也就是页表,这张表上记录了每一对页和页框的映射关系,能让CPU间接的访问物理内存地址。
总结一下,其思想是将虚拟内存下的逻辑地址空间分为若干页,将物理内存空间分为若干页框,通过页表便能把连续的虚拟内存,映射到若干个不连续的物理内存页。这样就解决了使用连续的物理内存造成的碎片问题。
2、物理内存管理
假设一个可用的物理内存有 4GB 的空间。按照一个页框的大小 4KB进行划分,4GB的空间就是4GB / 4KB = 1048576个页框。有这么多的物理页,操作系统肯定是要将其管理起来的,操作系统需要知道哪些页正在被使用,哪些页空闲等等。
struct page
结构
内核用struct page
结构表示系统中的每个物理页,出于节省内存的考虑,struct page
中使用了大量的联合体union。
struct page
{
// ....
};
其中比较重要的几个参数:
1、flags:用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。flag的每一位单独表示一种状态,所以它至少可以同时表示出32种不同的状态。这些标志定义在<linux/page-flags.h>
中。其中一些比特位非常重要,如PG_locked用于指定页是否锁定,PG_uptodate用于表示页的数据已经从块设备读取并且没有出现错误。
#define 未使用 (1<<0)
#define 正在使用 (1<<1)
#define 被锁定在内存中 (1<<2)
....
2、_mapcount:表示在页表中有多少项指向该页,也就是这一页被引用了多少次。当计数值变为-1时,就说明当前内核并没有引用这一页,于是在新的分配中就可以使用它。
3、virtual:是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(即所谓的高端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为NULL,需要的时候,必须动态地映射这些页。
page
结构管理数组
在 Linux 内核中实际上存在一个这样的数组 struct page mem_map[N]
,用该数组将每个页结构组织管理起来,将对页结构的管理转换为对数组这样的数据结构的增删查改工作。
在数组中,每个 page 结构都有自己的下标,而每个下标和page的地址可以相互转换:
- 物理地址 = 下标 * 4KB
- 下标 = 物理地址 / 4KB
既然 page
是用于描述物理内存的,则可以理解成:只要找到了 page,page 有下标,就等同于找到了物理内存!!
因此我们之前学习的:文件缓冲区,其实就是一个 page 的列表!!!
page
结构的内存存储
要注意的是struct page
与物理页相关,而并非与虚拟页相关。而系统中的每个物理页都要分配一个这样的结构体,让我们来算算对所有这些页都这么做,到底要消耗掉多少内存。
假设struct page
占 40 个字节的内存吧,假定系统的物理页为 4KB 大小,系统有 4GB 物理内存。那么系统中共有页面1048576个(1兆个),所以描述这么多页面的page结构体消耗的内存只不过 40MB,相对系统 4GB 内存而言,仅是很小的一部分罢了。因此,要管理系统中这么多物理页面,这个代价并不算太大。
要知道的是,页的大小对于内存利用和系统开销来说非常重要,页太大,页页必然会剩余较大不能利用的空间ÿ