当前位置:首页 > 公众号精选 > IOT物联网小镇
[导读]作 者:道哥,10年嵌入式开发老兵,专注于:C/C、嵌入式、Linux。关注下方公众号,回复【书籍】,获取Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章(PDF格式)。目录分段存储的坏处物理内存的管理映射表一个线性地址的寻址过程终于开始介绍分页机制了,作为一名L...

作  者:道哥,10 年嵌入式开发老兵,专注于:C/C 、嵌入式、Linux


关注下方公众号,回复【书籍】,获取 Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章( PDF 格式)。


目录


  • 分段存储的坏处


  • 物理内存的管理


  • 映射表


  • 一个线性地址的寻址过程



终于开始介绍分页机制了,作为一名Linuxer,大名鼎鼎的分页机制必须要彻底搞懂!


我就尽自己的最大努力,正确把我理解的分页机制,用图文形式彻底分解,希望对您有所帮助!


一共分 3 篇文章:


这篇文章主要介绍单映射表;


下一篇介绍两级映射(页目录和页表);


最后一篇介绍对映射表自身的操作。


分段存储的坏处

在之前的文章中,我们多次描写了一个段描述符的结构,其中就包括段的开始地址界限和各种段的属性


经过分段处理单元的权限检查和计算,这个开始地址加上偏移量,就是一个线性地址,如下图所示:


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!在x86系统中,分段机制是固有的,必须经过这个环节才能得到一个线性地址。


所以 Linux 系统中,为了“不使用”分段机制,但是又无法绕过,只好定义了“平坦”的分段模型。


在没有开启分页机制的情况下,分段单元输出的线性地址就等于物理地址。


这里就存在着一个重要的问题:从段的开始地址,一直到段空间的最后地址,这是一块连续的空间!


在这样的情况下,每一个用户程序中,包含的所有段,在物理内存上所对应的空间也必须是连续的,如下图:


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!因为每一个程序的代码、数据长度都是不确定、不一样的,按照这样的映射方式,物理内存将会被分割成各种离散的、大小不同的块。


经过一段运行时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,但是,这些物理内存都是以很多碎片的形式存在。


如果这个时候操作系统想分配一块稍微大一些的连续空间,虽然空闲的物理内存空间总数是足够的,但是不连续啊,这就给物理内存带来极大的浪费!


怎么办?


现在的需求是:操作系统提供给用户的段空间必须是连续的,但是物理内存最好不要连续。


软件领域有一句经典名言:没有什么是不能通过增加一个抽象层解决的!


在内存管理上,新加的这一层就是虚拟内存:把物理内存按照一个固定的单位(4 KB,称作一个物理页)进行分割,然后把连续的虚拟内存,映射到若干个不连续的物理内存页


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!图中绿色的的映射表,就是用来把虚拟内存,映射到物理内存。


物理内存的管理

关于映射表的细节,下一个主题再聊,先来看一下操作系统对物理内存的状态管理。


在如今的一台PC机上,内存动辄就是是8G/16G/32G的配置,好像很充裕、随便用。


但是在 N 年以前,买一个U 盘都是按照MB为单位的,更别说内存了。


因此在那个时代,面对MB级别的物理内存,操作系统还能够把它虚拟成4GB的内存空间给用户程序使用,也是挺厉害的!


言归正传,在这篇文章中,我们就奢侈一点,假设可用的物理内存有1GB的空间。


当系统上电之后,BIOS会检查系统的各种硬件资源,并告诉操作系统,其中就包括这1GB的物理内存。


按照一个物理页的大小4KB进行划分,1 GB的空间就是262144 (1GB / 4K)个物理页。


操作系统需要对这些页进行管理,也就是维护它们的状态:哪些页正在被使用,哪些页空闲


最简单、直观的方法,就是用一块连续的内存空间来描述每一个物理页的状态,每一个bit位对应一个物理页:


bit = 1: 表示该物理页被使用;


bit = 0:表示该物理页空闲;


262144个页需要262144个bit位,也就是32768个字节。


那么对于1 GB大小的物理内存来说,如下图所示:


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!利用map结构,操作系统就知道当前: 哪些物理页正在被使用,哪些物理页是空闲的。


  1. 每一个物理页是 4KB,所以地址中最后 12 个 bit 都是 0;


  2. map 结构本身也需要存储在物理内存中的,因此 32768 个字节,一共需要 8 个物理页来存储(32768 / 4 * 1024 = 8)。


映射表

在32位系统中,虚拟内存的最大空间是4GB,这是每一个用户程序都拥有的虚拟内存空间。


  1. 实际上,操作系统都会把虚拟内存的高地址部分,用作操作系统,低地址部分留给用户程序使用;


  2. Linux 系统中,高地址的 1GB 空间是操作系统使用;Windows 系统中,高地址的 2GB 的空间被操作系统使用,但是可以调整;


但是,实际的物理内存只有1GB(假设值),那么操作系统就要使用自己的腾挪大法,让用户程序认为4GB的内存空间全部可用。


就好比变戏法一样:十个碗,九个盖,谁能玩的溜、不露馅,谁就是高手!


计算一下映射表本身所占据的空间大小:


映射表中的每一个表项,指向一个物理页的开始地址。


在32位系统中,地址的长度是4个字节,那么映射表中的每一个表项就是占用4个字节。


既然需要让4GB的虚拟内存全部可用,那么映射表中就需要能够表示这所有的4GB空间,那么就一共需要1048576 (4GB / 4KB)个表项。


所以,映射表占据的总空间大小就是:1048576 * 4 = 4 MB 的大小。


也就是说,映射表自己本身,就要占用 1024 个物理页(4MB / 4KB)


正是因为使用一个映射表,需要占用这么大的物理内存空间,所以才有后面的多级分页机制。


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!虚拟内存看上去被虚线“分割”成4KB的单元,其实并不是分割,虚拟内存仍然是连续的


这个虚线的单元仅仅表示它与映射表中每一个表项的映射关系,并最终映射到相同大小的一个物理内存页上。


例如:


  1. 虚拟内存的 0 ~ 4KB 空间,对应映射表第 0 个表项中,其中存储的物理地址是 0x3FFF_F000(最后一个物理页);


  2. 虚拟内存的 4KB ~ 8KB 空间,对应映射表第 1 个表项中,其中存储的物理地址是 0x0000_0000(第 0 个物理页);


  3. 虚拟内存的最后 4KB 空间,对应映射表最后一个表项中,其中存储的物理地址是 0x0000_1000(第 1 个物理页);


也就是说:


虚拟内存与映射表之间,是平行的一一对应关系


映射表中的物理地址,与物理内存之间,是随机的映射关系,哪里可用就指向哪里(物理页)。


以上就是用一个映射表,把物理内存以4KB为一个页进行分配,然后再与虚拟内存对应起来,包装成连续的虚拟内存给用户使用。


虽然最终使用的物理内存是离散的,但是与虚拟内存对应的线性地址是连续的


处理器在访问数据、获取指令时,使用的都是线性地址,只要它是连续的就可以了,最终都能够通过映射表找到实际的物理地址。


为了有一个更加感性的认识,我们再来看一个稍微具象一点的实例。


一个线性地址的寻址过程

我们假设用户程序中有一个代码段,那么在这个程序的LDT(局部描述符表)中,段描述的结构如下:


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!假设条件如下:


  1. 虚拟内存(32位系统):4GB,实际的物理内存 1GB;


  2. 代码段的开始地址位于 3 GB 的地方,也就是 0xC000_0000;


  3. 代码段的长度是 1 MB;


我们的目标是:查找线性地址0xC000_2020所对应的物理地址。


根据描述符的结构,其中的段基地址是0xC000_0000,界限是0x00100,段描述符中,其它的字段暂时不用关心。


界限一共有 20 位,假设粒度是 4KB,那么 1 MB 的长度除以 4KB,结果就是 0x00100。


代码段的开始地址(线性地址)0xC000_0000,位于虚拟内存靠近高端四分之一的位置,那么映射表中对应的表项,也是位于高端的四分之一的位置。


映射表中每一个表项指向一个4KB大小的物理页,那么长度为1MB的代码段,就需要256个表项。


也就是说映射表中有256个表项,指向256个物理页:


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!对于我们要查找的线性地址0xC000_2020,首先把它拆解成两部分:


高 20 位 0xC0002: 是映射表索引;


低 12 位 0x020: 是物理页内的偏移地址;


索引值0xC002,对应于下图中从3GB开始的第2个表项:


Linux从头学14:【分页机制】-看了这篇文章还没彻底搞懂?我自罚三杯!在上面这个示意图中,代码段的开始地址0xC000_0000,对应于映射表中索引为0xC0000这个表项,这个表项中记录的物理内存页开始地址是0x1000_0000(距离开始地址256 MB)。


代码段的长度是1 MB,一共需要256个表项,那么最后这个表项的索引就应该是0xC00FF。


那么对于我们要寻找的线性地址0xC000_2020,对应的表项索引号是0xC0002,这个表项中记录的物理内存页的开始地址是0x2000_0000(距离开始地址512 MB)。


找到了物理内存的起始地址,再加上偏移量0x020,那么最终的物理地址就是:0x2000_0020。


以上就是通过映射表,从线性地址到物理地址的页转换过程。


对于使用二级页表的转换机制来说,原理都是一样的。无非是把高20位的索引拆开(10 位 10 位),使用两个表来转换,这个问题下一篇文章会详细聊。



------ End ------
本文描述了:通过一个映射表,把连续的虚拟内存,映射到离散的物理页,极大的利用了物理内存。


当操作系统需要分配一大块、连续的内存空间给用户程序时,映射表中的表项可以指向多个不连续的物理页,反正用户程序接触不到这一层(用户程序只与虚拟内存交互)。


这样利用物理内存的效率就极大的提高了。


再加上换出和换入机制(把硬盘当做物理内存来用),让用户程序以为有用不完的物理内存。


同时,我们也讨论了这个单一映射表的坏处,那就是映射表本身也占用了4MB的物理内存空间。


为了解决这个问题,伟大的先驱者们又引入了多级映射表(页目录表和页表),我们下一篇文章再见!


如果这篇文章对您有小小的帮助,请是转发给身边的小伙伴,让我们一起进步!



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭