17-分页

1. 概述

苦苦挣扎几近半月,保护模式中的段的篇章基本结束。迈过这个坑,踏过这个坎,前方还有千千万万个坑。只要坚持,一定可以走完。

本篇开始进入新的篇章——页。

不得不说,页在现代操作系统中起着无法动遥的地位,是实现现代操作系统的重要基础(当然,段也很重要)。你完全可以不用理解这句话,因为到目前为止,我们并没有进入操作系统正题。

2. 逻辑地址、线性地址与物理地址

咳咳咳…一下子冒出这么多概念真的好吗?忍耐,我从简说。

  • 逻辑地址,也有称作有效地址的。就是你在写 C 语言代码的时候,用到的指针里存的地址。
  • 线性地址,如果你还记得段选择子结构的话(不记得那就去把段选择子结构好好复习),其中有一个成员是 base,表示段基址。那么线性地址=ds.base+逻辑地址。(假设我用的是 ds 段寄存器)
  • 物理地址,通过某种映射方法,把线性地址映射到真实的物理内存条上的某个地址上。说的通俗点,就是通过一种计算方法,把线性地址当作参数,传到一个函数里,经过一通折腾,返回 来一个新的地址,这个新的地址就是物理地址,是真实的位于内存条上的地址。

[注] 实际上,最终计算的物理地址并不是真的就是真实物理内存上的地址,物理地址到真实内存条还有一层关系,然而对于我们来说,理解到这一层次足够了。

这里写图片描述

图1 逻辑地址到线性地址的映射
接下来,使用你的洪荒之力,把下面这张图看懂,能看懂多少是多少。

这里写图片描述

图2 逻辑地址最终转换成物理地址

至少,左半部分(Segmentation),相信你是没有问题的。右半部分(Paging),是今天的重点,我们要学的某种映射关系

3. 页

没错,这里还没到转换。因为你还不知道什么叫 page 。注意,这和操作系统没有关系,这是 INTEL CPU 提供的一种机制。

页你不懂,内存块总知道是啥吧。给内存块加个限定词——有固定大小的内存块,这就页。

问题是这个固定大小,到底是多大?CPU提供了两种大小,一种是 4KB 大小的内存块,称之为小页面,还有一种是多少,不记得了,以后再说吧。请把 4KB 大声朗读 3 遍。

4KB,也就是 2 12 2^{12} 212字节,也就是 0x1000 字节。

为了能够方便使用内存,CPU 把4GB地址空间(0x00000000~0xffffffff)划分成了 2 20 2^{20} 220 个,也就是1MB个固定大小的内存块,每一块的大小都是 0x1000 字节。第 0 页的范围是 0x0000000~0x00000fff,第二页的范围是0x00001000~0x00001fff,最后一页,也就是第 2 20 − 1 2^{20}-1 2201 页的范围是0xfffff000~0xffffffff

为了能够方便的把线性地址转换成物理地址,CPU把线性地址空间中的某一页,对应到物理地址空间的某一页。下图的每个颜色块都是 4KB,表示一页。

这里写图片描述

图3 线性页到物理页的映射

4. 如何映射

  • 页表

CPU把物理地址空间分成了若干个小页面(4KB),每个小页面都给了它一个编号,比如从编号从0~ 2 20 2^{20} 220-1(假设我的物理内存就是4GB)。写成 16 进制就是从 0x00000~0xfffff。这样,每个编号其实占用了 20 个比特位。

如果你的物理内存只有 1MB,可以分割成 2 8 2^8 28个小页面,那编号就是从 0- 2 8 2^8 28-1,16进制就是0x00000~0x000ff

无论如何,32位模式下,这个编号都要占用 20 个比特位。

只要知道到了小页面的编号,就一定能够计算出这些小页面的真实物理地址。比如 0 号物理页的物理地址就是 0x00000000,1 号物理页的地址是 0x000010000x000ff号的物理地址就是0x000ff000。这种计算方法很简单,只要把页面编号乘以 4KB 就行了,也就是乘以 4096,或者说左移 12 位,效果都是一样,因为每个小页面大小都是 4096 字节。

为了能让 CPU 找到这些小页面,势必要把这些小页面的“门牌号”保存起来。同样,这些“门牌号”也要保存在物理内存中,为了方便管理,再从物理内存中取出一页,来保存这些大小为 20 bit 的门牌号。

那么,一个内存页能保存多少个门牌号? 8 × 4 K B 20 \frac{8\times4KB}{20} 208×4KB 吗?没有,CPU不是这么干的。

CPU 用 32 bit (4字节)来保存一个页面编号,其中高20 bit 保存编号,低12位保存页面属性。目前你还不用关心这多出来的 12 bit 保存了啥属性。我们只关心其中的 20 bit 编号。

如此一来,一页可以保存 4 K B 4 B = 2 12 2 2 = 1024 \frac{4KB}{4B} = \frac{2^{12}}{2^2}= 1024 4B4KB=22212=1024 个“门牌号”.

我们把保存这种门牌号的页,称之为页表。而页表中的每一个元素占用4字节。一个页表可以保存1024个元素。如果用数组来表示页表,就是这样的

 int page_tables[1024]

这里写图片描述

图4 有一些物理页,专门用来保存页编号,称之为页表

有了这些已经足够了。接下来,就是如何把线性地址转换过去的问题。

  • 一级页表、二级页表和普通物理页

那些保存普通物理页索引号的页表,称为二级页表,而保存页表索引号的页表,称为一级页表,也叫页目录表。如图5。图中的 PDE意思是 page directory entry,即页目录表项;PTE的意思是 page table entry,即页表项。

这里写图片描述

图5 页目录、页表和普通物理页
  • 线性地址 10-10-12 分页

线性地址中保存的,就是页表中的索引号,也就是前面说的门牌号。一个线性地址是32位,它的结构是这样的。

|   31~22  |  21~12   |    11~0    | 比特
|9876543210|9876543210|ba9876543210| 比特
|----------|----------|------------| 占位
| 一级索引 | 二级索引 |  页内偏移  | 说明

可以看到,线性地址中保存了两个索引号和一个普通物理页偏移。

有线性地址 0x12345678,对应到上面的结构就是这样的。

|   31~22  |  21~12   |    11~0    | 比特
|9876543210|9876543210|ba9876543210| 比特
|----------|----------|------------| 占位
| 一级索引 | 二级索引 |  页内偏移  | 说明
|0001001000|1101000101|011001111000| <-- 线性地址0x1234567810-10-12 拆分

于是我们得到第一个页表索引号,它是 0001001000 = 0x48
第二个页表的索引号,它是 1101000101 = 0x345
最后一个是页内偏移,它是 011001111000 = 0x678

假如我们已经知道一级页表(这个通常称之为页目录)的基址page_dir_tables.
那么根据一级索引,我们得到 page_dir_tables[0x48] 的值,假设 page_dir_tables[0x48] = 0x12fff067

前面说过,页表中每一项的高20位保存的都是另一个物理页的编号。

根据规则,0x12fff067 的高20位是二级页表编号,即0x12fff,于是计算得到这个物理页基址是0x12fff000.

为了能找到这一页中索引号 0x345 这个位置的值,令 int *page_tables = 0x12fff000page_tables[0x345]=0x21991067,那么这个值中告诉我们的页编号是 0x21991,换算成页基址是 0x21991000

最终我们定向到了物理页 0x21991000,这个物理页保存的也不是页面编号了,就是普通数据,它是普通页。另外,线性地址的第 3 部分,是页内偏移,把这个普通页的基址+页内偏移,最终得到了物理地址,也就是 0x21991000+0x678=0x21991678

现在回过头来,再看看图2,相信你应该能看懂了。

5. 总结

本篇简单介绍了 10-10-12 分页,一个线性地址按照10-10-12分成三段:

第一段,一级页表索引号
第二段,二级页表索引号
第三段,普通页页内偏移

实际还有另一种2-9-9-2分页,这是以后的事情。看完这篇后,或许你还有一个疑问,就是一级页表,也就是页目录表的基址CPU是怎么知道的?

实际上在CPU中,有一个寄存器,称之为 CR3 寄存器,它保存了一级页表的基址。

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
以下是使用 ElementUI 中的 el-pagination 实现的纯前端分页代码: ```html <template> <div> <!-- 显示当前数据 --> <ul> <li v-for="item in currentPageData">{{ item }}</li> </ul> <!-- 分页器 --> <el-pagination v-show="total > pageSize" :current-page="currentPage" :page-size="pageSize" :total="total" @current-change="handleCurrentChange" /> </div> </template> <script> export default { data() { return { // 总数据量 total: 50, // 每显示的条数 pageSize: 10, // 当前数 currentPage: 1, // 当前的数据 currentPageData: [] }; }, computed: { // 计算总数 totalPage() { return Math.ceil(this.total / this.pageSize); } }, methods: { // 处理数改变事件 handleCurrentChange(newPage) { this.currentPage = newPage; this.getCurrentPageData(); }, // 获取当前的数据 getCurrentPageData() { const start = (this.currentPage - 1) * this.pageSize; const end = start + this.pageSize; this.currentPageData = this.allData.slice(start, end); } }, mounted() { // 所有数据 this.allData = ['数据1', '数据2', '数据3', '数据4', '数据5', '数据6', '数据7', '数据8', '数据9', '数据10', '数据11', '数据12', '数据13', '数据14', '数据15', '数据16', '数据17', '数据18', '数据19', '数据20', '数据21', '数据22', '数据23', '数据24', '数据25', '数据26', '数据27', '数据28', '数据29', '数据30', '数据31', '数据32', '数据33', '数据34', '数据35', '数据36', '数据37', '数据38', '数据39', '数据40', '数据41', '数据42', '数据43', '数据44', '数据45', '数据46', '数据47', '数据48', '数据49', '数据50']; // 初始化当前的数据 this.getCurrentPageData(); } }; </script> ``` 在这段代码中,我们使用了 ElementUI 中的 el-pagination 组件来实现分页器的功能。在模板中,我们使用 v-for 指令来循环显示当前的数据,并使用 el-pagination 组件来显示分页器。在脚本中,我们定义了一些数据和方法来实现分页的逻辑,其中 getCurrentPageData 方法用于获取当前的数据。在 mounted 钩子函数中,我们初始化了所有数据,并将当前的数据设置为第一的数据。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值