当前课程知识点:Linux 内核分析与应用 > 第2章 内存寻址 > 2.4 动手实践-把虚拟地址转换成物理地址 > Video
大家好今天跟大家分享的题目是动手实践把虚拟地址转换为物理地址
在开始之前先跟大家一起回顾一下相关的概念
在前面的学习中我们了解到在进程中我们不直接对物理地址进行操作
CPU在运行 指定的地址要经过MMU转换后才能访问到真正的物理内存
地址的转换过程分为两块 分段和分页
分段机制简单的来说是将进程的代码 数据 栈分在不同的虚拟地址段上
从而避免进程间的互相影响 分段之前的地址我们称之为逻辑地址
它由两部分组成 高位的段选择符和低位的段内偏移
在分段时先用段选择符在相应的段描述符表中找到段描述符 也就是某一个段的基地址
再加上段内偏移量就得到了对应的线性地址
线性地址也称之为虚拟地址
而在实际的应用中 Linux系统为了增加系统的可移植性并没有完整的使用分段机制
它让所有的段都指向相同的地址范围
段的基地址都为0 这样逻辑地址和线性地址在数值上就相同了
所以今天我们分析的重点是在分页
也就是由线性地址到物理地址转换的过程
Linux为了兼容32位和64位的CPU呢
它需要一个统一的页面地址模型 目前常用的是4级页表模型
里面有4级页表 PGD页全局目录 页上级目录 页中间目录和页表
根据不同的需要 其中某些页表可能未被使用
线性地址中每一部分索引
的大小会根据具体的计算机体系结构
做相应的改变举个例子来说 对于没有启用物理地址
扩展功能的32位系统来说 两级页表就足够了
那么Linux会让首先使线性地址中
将页上级目录和页中间目录索引
这两位置为0 从根本上就取消了这两个字段
但是这两个页目录在指针序列中的位置仍然被保留下来
也就是说寻址过程中呢
不能跳过页上级目录和页中间目录直接由页全局目录直接到
页表内核会将这两个
页目录的表项呢都置为1
由于64位处理器硬件的限制
它的地址线只有48条
所以线性地址实际使用的只有48位
在64位Linux中使用了4级页表结构
它的线性地址划分如上图所示 首先
页全局目录的索引 页上级目录的索引
页中间目录的索引和页表的索引分别占了9位
最后页内偏移占了12位 共计是48位
剩下的高位都是保留 留作以后扩展使用的
在这种情况下页面的大小都为4kb
每一个页表项大小为8bit 整个页表可以映射的空间是256TB
而新的Intel芯片的MMU硬件规定可以进行5级的页表管理
所以在4.15的内核中呢
Linux已经在这里增加了一个新的页目录 也就是在
页全局目录和页上级目录之间又增加了一个新的页目录 叫做p4d页目录
这个页目录同32位中的情况一样现在还未被使用
它的页目录项只有一个 线性地址中也没有它的索引位
在这里我们看到一个很重要的寄存器 就是CR3寄存器
它是一系列CPU控制寄存器之一
这些控制寄存器主要用来保存控制系统级别操作的数据和信息的
其中这个CR3就是用来保存当前进程的页全局目录的地址的
寻页的开始就是从页全局目录开始 那么页全局目录的地址又在哪呢
内核在创建一个进程时就会为它分配页全局目录
在进程描述符task_struct结构中有一个指向mm_struct结构的
指针 mm
而mm_struct结构是用来描述进程的虚拟地址空间的
在mm_struct中有个字段PGD
就是用来保存该进程的页全局目录的(物理)地址的
所以在进程切换的时候呢 操作系统通过访问task_struct结构
再访问mm_struct结构 最终找到PGD字段 取得新进程的页全局目录的地址
填充到CR3寄存器中就完成页表的切换
好了了解了这些之后 我们在实际的系统中来看看寻页的过程是如何完成的
为了能够看到寻页机制的过程 我们需要使用内核提供的一些函数 为此呢我们
编写一个内核模块 我们先来看一下它的代码
该内核模块的主要功能是在内核中先
申请一个页面 然后利用内核提供
的函数按照寻页的步骤一步步查询各级页目录
最终找到所对应的物理地址
这些步骤就像我们手动模拟了MMU单元的寻页过程
首先看到的这个函数get_pgtable_macro
它的作用是打印页机制中一些重要参数的
CR3寄存器的值通过这个read_cr3_pa函数来获得
接着是一些_SHIFT这些宏
这些宏的作用是用来指示线性地址中
线性地址中相应字段所能映射区域大小的对数的
PAGE_SHIFT就是指示page offset字段
所能映射区域大小的对数 page offset字段我们都知道它映射的是一个页面的大小
而一个页面的大小是4K 转换成以2为底的对数就是12
其他的SHIFT宏的作用类似
下面这个宏(PTRS_PER_x)是用来指示相应的页目录表中项的个数的
最后这个宏是page_mask 是所谓的页内偏移掩码
页内偏移掩码是用来屏蔽掉page offset字段的
这些宏都是为了方便寻页时进行位运算的
我们后面要用到它们 这里先把它们打印出来看一下
接下来这个函数就是我们用来进行线性地址到物理地址转换的函数
首先呢我们是用
为每个页目录项创建一个变量来将他们保存起来
接着我们使用的第一个函数呢就是这个
pgd_offset()函数
它传入的第一个参数是当前进程的mm_struct结构
这里我们想一下 因为我们申请的线性地址是在内核空间的 所以我们要查的
页表也应该是内核页表 但是呢
我们知道所有的进程都共享同一个内核页表 所以可以用当前进程的
mm_struct结构来进行查找 我们
查找得到了第一个
页全局目录项PGD之后 我们将其作为
下级查找的参数传入到这个p4d_offset中
做下一级的查找 我们就找到了P4d
这个页目录项
然后再去找相应的PUD页目录项 PMD页目录项 最终我们就会找到
我们的PTE页表项了
在查找页表项的时候 这个函数与上面这些函数略有不同
它是pte_offset_kernel 这是为了表示我们查找是在
内核页表主内核页表中查找的
而在进程页表中查找是有一个完全不同的一个函数的
最后我们就取得了页表的线性地址了
接着呢我们需要从
这个页表的线性地址也就是这个页表项中取出
该页表所映射页框的物理地址
我们是这样做的 将其与PAGE_MASK
这个变量进行位或的操作 取出其高48位
就得到了页框的物理地址 接着我们需要取出页偏移地址
页偏移量也就是
线性地址中的低12位 我们是
这样做的 将PAGE_MASK按位取反
然后与我们的vaddr做了一个位或的操作
这样就取出了它的页内偏移 最后我们将这两个地址
拼接起来 就得到了我们想要的物理地址了
最后我们将这些地址都打印出来 就完成线性地址到物理地址转换的过程了
最后这两个函数就是内核模块的注册和卸载函数
在注册函数中我们完成了内核模块的初始化
首先我们打印了这些主要的这些参数
然后就是使用这个get_free_
page这个函数的在内核的
ZONE_NORMAL中申请了一块页面
后面这个标志是用来指示 它是优先从内存的ZONE_NORMAL区中申请页框的
这里我补充一下 在64位系统中已经不再有所谓的高端内存的概念了
这是因为我们使用的内存的大小是比较小的
线性映射已经可以将所有的物理内存映射到线性地址空间了
所以64位体系架构上的ZONE_HIGHMEM区总是为空的
这里我们在这个地址中我们
写入一段话“hello world from kernel” 这我们后面是有用的
最后我们将调用这个线性地址到物理地址转换的函数完成
线性地址到物理地址的转换 最后我们在这个
内核模块卸载函数中将我们申请的线性地址空间释放掉
接着看一下MAKEFILE文件
MAKEFILE文件也比较简单 可以用之前写过的HELLO WORLD的
MAKEFILE文件就可以 只要将名字改一下就可以 接着我们来MAKE一下
然后我们将其插入到内核中 我们在内核中看一下它打印出来的log
这就是我们那个模块运行的那个
打印出来的参数 首先 我们的CR3寄存器的值在这里然后我们的
每一个SHIFT宏就在这里 我们可以看到
我们的P4D_SHIFT和我们的PGD_SHIFT它
都是39的这也就意味着在线性地址中
我们的P4D这个字段它是为空的
我们也可以看到在页目录项中 P4D的页目录项也是为1的
这就跟我们之前讲的是一样的 虽然我们Linux现在用了五级的
页表模型 但实际上使用的页表也只有四个
最后是这个PAGE_MASK 它是低12位都为零
其余位都为1的一个64位的数
我们申请到的这个线性地址呢 这是它的线性地址
我们依次查找了
它的PGD也就是页全局目录项的线性地址
页四级目录项的线性地址 页上级目录项的线性地址
还有页中间目录项的地址和最后的
页表项的物理地址 这些页目录项偏移我们都在后面打印了出来
最后我们将这个线性地址转化成了物理地址 是后面这个
我们可以看到这个线性地址 它的第一位是8 转换到二进制呢
就是最高位63位是为1的 这是一个X86平台上用来
标识该物理页框是不能用来执行代码的一个保护位的
这里我们不去管它 其物理页框的物理地址就是后面这九位
好了 到这里我们就完成了这个
代码的演示过程
但到这里我觉得还不够过瘾 因为我们看到的只是一些数字的变化
并没有直观地感受到线性地址和物理地址之间的这个关系
接下来我给大家来看一个更有趣的一个东西
还记得我们在刚才
内核中申请线性地址空间的时候 我们在这个线性地址中呢
输入了一段话 现在我们已经得到了该线性地址所对应的物理地址了
我们现在就想啊 能不能直接访问一下这个物理地址 看看里边
存放的东西到底是什么 如果是我们刚才
输入的那段话 也就证明了我们这个寻页的程序是正确的
所以为了做到这一点我们需要两个小工具来帮助一下
这两个小工具是我在网上找的 它最早呢是
07年的一个旧金山大学的高级系统编程
课程上为学生提供的一个
小工具 首先是一个DRAM的一个内核模块 我们先来看一下它的源码
这是它的源码 它主要的
功能呢就是通过
mmap将物理内存中的数据映射到
我们的一个设备
文件中 我们通过对于这个设备文件进行访问 就可以达到
访问物理内存的功能了
另一个工具叫做fileview 它可以让我们对
按照我们想要的一种格式阅读这种二进制文件
这两个小工具都比较老了 我们在用的时候需要先对它们进行一些修改
修改之后我们拿到这两个工具 就可以对我们物理内存进行
在用户空间下对物理内存进行访问了
那么先做的呢就是 将这个
模块编译好之后将它插入到内核中 然后呢再
dev目录下创建这样一个文件
设备文件然后它的设备号在源码中提到了 我们输入进去就行
做好了之后 我们就可以用我们的fileview
工具直接去访问这个DRAM的文件
就相当于我们直接可以阅读到物理内存中的数据了
我们来看一下刚才申请的那个线性地址
在这里我们将它转化成二进制 然后将其中每一个
索引字段都转化成
二进制提取出来我这里已经给大家算好了
首先PGD字段是这样这是它的二进制
从39位开始 一直往上占了九位
转化成十进制就是这个284 也就是这个pgd_index
我们知道每一个页目录项它们的大小都是8比特 所以我们要
在这个索引的基础上再乘一个8
我们才能得到在物理地址中它的偏移量
我们将它转化成16进制就得到了
该索引位在物理地址中的偏移量 有了这个之后呢
我们就可以
在物理内存中查找相应的数据了
首先 第一步我们是在页全局目录中
按照这个索引进行查找的 那么我们的页全局目录表呢
它的基地址在哪呢 就在我们的这个CR3寄存器中保存着
所以我们用CR3寄存器的值加上这个偏移量 我们看看里边有什么
好了 我们可以看到在这个
地址下呢 我们找到了这样的数据
7CD3F067 这是什么呢 我们来看一下
这不就是我们的pgd_val打印出来的数据吗
我们还记得pgd_val打印出来的是什么呢 就是我们的PGD
页全局目录项里边所存放的数据
也就是下一级页表的物理地址
有了这个之后我们就可以继续的去查找
我们下级查找就是按照PUD的偏移量来去查找
我们的基地址我们已经查到了7CD3F000
然后偏移量是998
998在后面这块这是990 998在这 好了我们看到了
查到了下一页表的起始地址25FFFD067 也就是我们的
pud_val打印出来的这个数据
我们看到在物理内存读出来跟我们打印出来的都是能对上的
所以我们的寻址过程呢
按这样的寻址是正确的 最后我们得到了
就是物理地址了
这里给大家来补充一下 我们大家都看到这些
目录项中查到了地址 它的结尾都是067 063这是为什么呢
因为我们的这个相应的这个
每一个目录项它的低12位其实是没用的 因为我们最后呢都其实是要
将它与页内偏移做一个拼接的
所以这低12位空出来 我们用来存放的是
相应的页目录项或者页表的属性
所以呢 我们在加这个
进行地址的加减的时候将它们置为0就可以了
最后我们的物理地址在这里 我们直接将这个物理地址的
输进去 看看里边是什么
我们看到了 “hello world from kernel”
就是我们在最开始的那个线性地址中存储的那段话
这也就证明了这个物理地址转化的程序是正确的
好了 今天的分享就到这里了 谢谢大家
-1.1 Linux操作系统概述
-1.2 Linux内核结构以及内核模块编程
--Video
-1.3 Linux内核源码中的双链表结构
--Video
-1.4 源码分析-内核中的哈希表
--Video
-1.5 动手实践-Linux内核模块的插入和删除
--Video
-第1章 概述--章节测验
-2.1 内存管理之内存寻址
--Video
-2.2 段机制
--Video
-2.3分页机制
--Video
-2.4 动手实践-把虚拟地址转换成物理地址
--Video
-第2章 内存寻址--章节测验
-3.1 进程概述
--Video
-3.2 Linux进程创建
--Video
-3.3 Linux进程调度
--Video
-3.4 动手实践-打印进程描述符task_struct中的字段
--Video
-3.5工程实践-基于内核模块的负载监控
--Video
-第3章 进程管理--章节测验
-4.1 Linux内存管理机制
--Video
-4.2 进程用户空间管理机制
--Video
-4.3 物理内存分配与回收机制(上)
--Video
-4.4 物理内存分配与回收机制(下)
--Video
-4.5 动手实践-Linux内存映射基础(上)
--Video
-4.6 动手实践-Linux内存映射实现(中)
--Video
-4.7 动手实践-Linux内存映射测试(下)
--Video
-4.8 初学者对内存管理的常见疑惑
-第4章 内存管理--章节测验
-5.1 中断机制概述
--Video
-5.2 中断处理机制
--Video
-5.3 中断下半部处理机制
--Video
-5.4 时钟中断机制
--Video
-5.5 动手实践-中断上半部的代码分析及应用
--Video
-5.6 动手实践-中断下半部的代码分析及应用
--Video
-第5章 中断--章节测验
-6.1 Linux中的各种API
--Video
-6.2 系统调用机制
--Video
-6.3 动手实践-添加系统调用(系统调用日志收集系统)
--Video
-第6章 系统调用--章节测验
-7.1 内核同步概述
--Video
-7.2 内核同步机制
--Video
-7.3 动手实践-内核多任务并发实例(上)
--Video
-7.4 动手实践-内核多任务并发实例(下)
--Video
-第7章 内核同步--章节测验
-8.1 虚拟文件系统的引入
--Video
-8.2 虚拟文件系统的主要数据结构
--Video
-8.3 文件系统中的各种缓存
--Video
-8.4 页高速缓存机制以及读写
--Video
-8.5 动手实践-编写一个文件系统(上)
--Video
-8.6 动手实践-编写一个文件系统(中)
--Video
-8.7 动手实践-编写一个文件系统(下)
--Video
-第8章 文件系统--章节测验
-9.1 设备驱动概述
--Video
-9.2 I/O空间管理
--Video
-9.3 设备驱动模型
--Video
-9.4 字符设备驱动程序简介
--Video
-9.5 块设备驱动程序简介
--Video
-9.6 动手实践-编写字符设备驱动程序
--Video
-9.7工程实践-编写块设备驱动的基础(上)
--Video
-9.8 工程实践-块设备驱动程序分析(中)
--Video
-9.9 工程实践-块设备驱动程序实现(下)
--Video
-第9章 设备驱动--章节测验
-致谢与说明
--Video