2.1 Linux 内存管理的基本框架
2.2 地址映射的全过程
Linux 会在不同的 CPU 上运行, 相应的 80386 也不仅仅只会跑 Linux 系统, 所以系统和 CPU 之间的配合并不是完美的.
2.2.1 逻辑地址到线性地址
逻辑地址到线性地址即段映射阶段. 假设整个系统的映射机制都已经建立好, CPU 正在执行 call 08048368 去执行另一段代码.
在段映射阶段首先要根据段寄存器里的值去选择一个合适的段描述符. CPU 根据所执行指令的不同去选择不同的段寄存器, 由于这里的 call 指令是去执行别的代码段的内容, 所以采用 CS 寄存器里的值作为段选择符.
那么此时此刻 CS 里的值是多少? CS 的值是在什么时候被设置成多少, 又会在什么时候被修改? 内核在新建一个进程的时候需要将该进程所需要使用的段寄存器全部设置好, 代码如下:
411 到 414 是用来设置 ds es ss cs 寄存器值的地方, 后面的宏定义的常量是被设置成的值. 可以看出两个很有意思的地方: 1, 除了 cs 寄存器外, 其余的几个寄存器都是一样的值 2, 把宏常量设置成寄存器的值, 所以所以进程的 cs ds es ss 内容是一样的. Intel 工程师分出来的代码段, 数据段, 堆栈段到了 Linux 这里只剩下了代码段和数据段.
整个 Linux 内核里就四个段选择符, 如下所示, 并且从 TI=0 可以看出全都放在了 GDT 表里了, 而且在 Linux 里基本上不使用 LDT.
回到程序, 现在段映射需要做的是去 GDT 表里把 index=4 的段描述符拿出来, GDT 表的定义和里面的段描述符如下所示.
总的来说这四个段的基址都是 0x0, 长度都是 0xffffff , 并且都在内存里, 也就是是说内核不会把整个段全部置换到硬盘上. 他们的不同之处主要体现的访问的权限 (DPL) 和该段存储的是数据还是代码. 虽然看到现在 Linux 的段式映射徒有其表, 但是在这个地方他还是完成了权限的检查. 比如段描述符要求的 DPL 为 0, 但是 CS 寄存器里的 DPL 为 3, 那么这次访问会被禁止. 但其实这个检查在后面的页映射还会再做一次.
内核在新建一个进程的时候, 为其分配 CS 寄存器的值(虽然是固定的),CS 寄存器的一部分用于定位段描述符, 另一部分用于规定该进程的权限, 即可访问的存储区域是属于内核还是用户. 从这个角度来看进程是被内核安全的管理的, 一个进程在创建的时候就规定好了其权限.
2.2.2 线性地址到物理地址
经过段式映射, 一个虚拟地址被映射成线性地址, 而且这个线性地址和虚拟地址一样, 因为基址都是 0. 但最起码从法理的角度现在他是线性地址了, 整个过程就是在欺骗 80386.
Linux 里线性地址到物理地址的映射需要经过三个步骤, 这一点是和 80386 不一样, 80386 只有两个步骤. 这个是 Linux 虚拟的映射模型, 其实和 80386 原理都是一样的, 只不过多了一个步骤. 为了解决这个矛盾, 在 Linux 编译的时候回根据 CPU 的型号选择相应的编译版本, 在 80386 上运行的时候回选择 32 位地址的两层映射. 其实从这里很好的看出来的虚拟模型和实际模型的区别, 虚拟模型是 3 层的但是受限于硬件只能改成两层的.
下面这个文件定义了如何去使用 32 位的线性地址, 也就定义了如何从线性地址映射到物理地址. 第一个 PGDIR_SHFIT 定义了 PGD 索引的起始位置, bit22 到 bit31 一共 10 位, 索引是 10 位的, 所以一个 PGD 表里可以有的 PGD 的个数是 1024 个. 接下来的 PMD_SHIFT 也是 22, 从 bit22 到 bit22 就是一位也没有, 也就是说 PMD 只有一个, 这样以来三层映射到了 386 这里变成了两层, 其实从结果来看就是跳过了 PMD 这一层, 但在 Linux 源码的角度来说是三层映射.
整个地址的映射是由 MMU 来管理的, 所以虽然说是 CPU 来完成了整个地址转换过程, 但是其实本质上是 MMU 来完成的, 在 80836 上 Linux 的页式管理如下.
在一个进程上处理机的时候, 内核会使用特权指令设置好 PGD 的 BASE, 存在寄存器 CR3 里, 这个 PGD 的地址是进程独享的, 也就是每一个进程的 PGD 的地址不一样, 这一点和段式映射都使用同一个 GDT 不一样. 然后 MMU 用线性地址的第一个位段即高 10 位在 PGD 里找到相应的项, 然后 MMU 用线性地址中的 PT 段作为下表在 PGD 表项里找到相应的 PTE, 在 PTE 里存的是就是物理地址的 BASE, 然后线性地址剩下的部分用于和物理地址的 BASE 组合得到真正的物理地址.
整个映射过程 CPU 要访问三次内存: 取 PGD, 取 PT, 取 PTE, 所以虚存的高效实现要依赖于缓存.
2.3 几个重要的数据结构和函数
在整个逻辑地址到物理地址的映射过程中主要是由 MMU 和几个寄存器的来完成, Linux 需要做的是在一个进程上处理机的时候设置好相应的寄存器的值和在内存中存放合理的数据, 比如设置 gdtr 的值和 GDT, 准备好 PGD,PT. 本节介绍的是这个 Linux 在管理内存映射的时候需要用几个函数和数据结构.
在页式映射阶段使用的 PGD PMD PT 定义如下, 我们当然使用上面的那部分结构体的定义.
页式映射的最后一站是 PTE 到物理地址的映射, PTE 的定义包含了两个部分, 32 位的 PTE 指针高 20 位是作为指针来使用, 低 12 位用来表示页面的状态信息和访问权限, 具体来说是 9 位用来表示权限和标志位. 这些权限标志位表示该地址是否在内存中, 其权限大小, 是否可以使用缓存等等,
内核里有一个全局变量指向一个 page 数组, 每一个 page 代表一个物理页面, 那么整个 page 数组就代表整个物理页面. 所以 PTE 的高 20 是这个数组的索引, 拿着这个索引就可以在数组里定位到想要的物理页面. 这个索引定位的过程是从软件的角度来说的, 从硬件的角度来说, MMU 需要把 20 位的地址和剩下的访问权限结合在一起然后去物理内存上定位并检验权限, 事实上高 20 位补上 12 个零就是就是物理页的起始地址. 如果 MMU 的工作过程正常结束, 那么通过索引下标访问 page 数组的过程就正常结束, 反之就会抛出相应的异常.
MMU 在进行在拿着高 20 位进行映射前会先检查其权限, 在检查权限的过程中会首先检查 P 标志位, 即_PAGE_PRESENT, 该位为 1 的时候表示对应的物理页在内存中, 后面才会继续. 这是一种物理页定位失败的情况, 除此之外还有另一种失败的情况. 在拿着 32 为的线性地址一步步定位 PTE 地址的时候, 发现 PTE 整个内容为 0, 则说明该物理内存和虚拟内存的映射还没有建立成功.
来源: http://www.bubuko.com/infodetail-2956946.html