当前位置: 首页 > news >正文

Linux内核学习笔记——页表的那些事。

目录

  • 页表什么时候创建
  • 内核页表变化什么时候更新到用户页表
    • 源码分析
  • 常见问题解答
    • 问题一:页表到底是保存在内核空间中还是用户空间中?
    • 问题2:页表访问,软件是不是会频繁陷入内核?
    • 问题3:内存申请,软件是不是会频繁陷入内核创建新页表条目
    • 问题4:那内核页表和普通的页表到底有什么区别?

接上两文,本文补充一下内核页表和用户页表创建、更新时机说明。
Linux内核学习笔记——内核页表隔离KPTI机制
Linux内核学习笔记——内核页表隔离KPTI机制(源码分析)

KPTI中每个进程有两套页表——内核态页表与用户态页表(两个地址空间)。

内核态页表只能在内核态下访问,可以创建到内核和用户的映射(不过用户空间受SMAP和SMEP保护)。

  • 内核页表:即书上说的主内核页表,在内核中其实就是一段内存,存放在主内核页全局目录init_mm.pgd(swapper_pg_dir)中,硬件并不直接使用。

  • 进程页表:每个进程自己的页表,放在进程自身的页目录task_struct.pgd中。

在保护模式下,从硬件角度看,其运行的基本对象为“进程”(或线程),而寻址则依赖于“进程页表”,在进程调度而进行上下文切换时,会进行页表的切换:即将新进程的pgd(页目录)加载到CR3寄存器中。从这个角度看,其实是完全没有用到“内核页表”的,那么“内核页表”有什么用呢?跟“进程页表”有什么关系呢?

页表什么时候创建

内核页表中的内容为所有进程共享,每个进程都有自己的“进程页表”,“进程页表”中映射的线性地址包括两部分:

  • 用户态
  • 内核态
    其中,内核态地址对应的相关页表项,对于所有进程来说都是相同的(因为内核空间对所有进程来说都是共享的),而这部分页表内容其实就来源于“内核页表”,即每个进程的“进程页表”中内核态地址相关的页表项都是“内核页表”的一个拷贝(进程创建时候就产生了)。

内核页表变化什么时候更新到用户页表

“内核页表”由内核自己维护并更新,在vmalloc区发生page fault时,将“内核页表”同步到“进程页表”中。以32位系统为例,内核页表主要包含两部分:

  • 线性映射区
  • vmalloc区

其中,线性映射区即通过TASK_SIZE偏移进行映射的区域,对32系统来说就是0-896M这部分区域,映射对应的虚拟地址区域为TASK_SIZE~TASK_SIZE+896M这部分区域在内核初始化时就已经完成映射,并创建好相应的页表,即这部分虚拟内存区域不会发生page fault

vmalloc区,为896M~896M+128M,这部分区域用于映射高端内存,有三种映射方式:vmalloc、固定、临时,这里就不详细展开了。

以vmalloc为例(最常使用),这部分区域对应的线性地址在内核使用vmalloc分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新。

源码分析

/*
     * 缺页地址位于内核空间。并不代表异常发生于内核空间,有可能是用户
     * 态访问了内核空间的地址。
     */
    if (unlikely(fault_in_kernel_space(address))) {
        if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {
            //检查发生缺页的地址是否在vmalloc区,是则进行相应的处理
            if (vmalloc_fault(address) >= 0)
                return;

/*
  * 对于发生缺页异常的指针位于vmalloc区情况的处理,主要是将
  * 主内核页表向当前进程的内核页表同步。
  */
static noinline __kprobes int vmalloc_fault(unsigned long address)
{
    unsigned long pgd_paddr;
    pmd_t *pmd_k;
    pte_t *pte_k;
 
    /* Make sure we are in vmalloc area: */
    /* 区域检查 */
    if (!(address >= VMALLOC_START && address < VMALLOC_END))
        return -1;
 
    WARN_ON_ONCE(in_nmi());
 
    /*
     * Synchronize this task's top level page-table
     * with the 'reference' page table.
     *
     * Do _not_ use "current" here. We might be inside
     * an interrupt in the middle of a task switch..
     */
     /*获取pgd(最顶级页目录)地址,直接从CR3寄存器中读取。
     *不要通过current获取,因为缺页异常可能在上下文切换的过程中发生,
     *此时如果通过current获取,则可能会出问题*/
    pgd_paddr = read_cr3();
    //从主内核页表中,同步vmalloc区发生缺页异常地址对应的页表
    pmd_k = vmalloc_sync_one(__va(pgd_paddr), address);
    if (!pmd_k)
        return -1;
    //如果同步后,相应的PTE还不存在,则说明该地址有问题了
    pte_k = pte_offset_kernel(pmd_k, address);
    if (!pte_present(*pte_k))
        return -1;
 
    return 0;
}

常见问题解答

问题一:页表到底是保存在内核空间中还是用户空间中?

创建和删除页表的确是在内核空间操作的。页表不能在用户空间进行操作一点都不奇怪,你要知道页表的作用不仅仅是虚拟地址到物理地址的映射,还有关键的权限访问控制和页面属性的记录。下图是armv8中level 1的页表格式,类似于x86中的PUD的结构:
在这里插入图片描述
可以看到该页表中只有"Outlook block address"是在表示下一级页表的地址,"Upper attributes"和"Lower attributes"是内核空间用到权限的控制位和页属性标志。

问题2:页表访问,软件是不是会频繁陷入内核?

这个需要结合场景分析。访问页表是否会陷入内核,这要看你是:

  1. CPU地址翻译的过程中的页表访问;
  2. 增加修改页表项。

如果是第一种,CPU地址翻译,那么这种访问是硬件完成的,整个过程不需要代码参与,没有任何性能上的损失。

如果是第二种,是会慢一些。这种慢是为了安全,如果页表在用户空间,那么用户就可能自己修改页表,映射任意的内存地址,访问任何内存,甚至是直接操作硬件,进程间、内核的隔离保护就失去了意义。

问题3:内存申请,软件是不是会频繁陷入内核创建新页表条目

你以为在用户进程中分配内存的时候,就马上通过系统调用陷入内核,然后进行页表操作吗?这个理解是不对的。

应用程序虽然可能频繁的malloc或者free,但在页表层面上,并不会频繁的创建、删除页表项,主要原因是,malloc/free操作的接口都是C库的接口,在C库里,还有另外一层次的封装,来保证不会频繁的提交页表的操作申请。

内核如今已经发展的很成熟了,当然不会这么傻。在你兴高采烈的分配好一块内存后,内核只是给你找了一块独一无二的虚拟内存空间,并没有映射到物理内存,所以根本没有页表的操作。只有你真正用到你的内存时,MMU发现无法进行虚拟内存到物理内存的转换,只好抛出page fault异常,然后进入内核进行物理内存的分配过程,接着就给你把页表创建好了,这个整个过程叫做惰性分配

更重要的是,其实libc库在进程创建的时候,就已经把堆空间用内存池的方式管理起来,在进程分配小于128kb的内存时,根本不需要内核进行任何操作,因为堆这个段的虚拟内存早就映射好了物理内存

问题4:那内核页表和普通的页表到底有什么区别?

对于所有进程来说它们页表中的内核空间页表部分都是一模一样的,它们都是从1号进程的init_mm结构中copy的,只有用户空间的页表不尽相同。用户空间的页表是用来进行不同进程地址空间隔离的,所以相同的虚拟地址可以映射到不同的物理地址,当然一般情况下这也是必须的,而内核只有一个。

相关文章:

  • 一文带你入门,领略angular风采(上)!!!
  • 嵌入式学习笔记——STM32硬件基础知识
  • 2023年“网络安全”赛项浙江省金华市选拔赛 任务书
  • Qt安装与使用经验分享;无.pro文件;无QTextCodec file;Qt小试;界面居中;无缝;更换Qt图标;更换Qt标题。
  • MyBatis常用的俩种分页方式
  • Python---正则表达式
  • 基于flask+bootstrap+echarts+mysql的鱼村小馆订餐后台管理系统
  • 逻辑优化-rewrite
  • ECharts可视化库--常用组件
  • 【FPGA】Verilog:时序电路应用 | 序列发生器 | 序列检测器
  • 【软件使用】MarkText下载安装与汉化设置 (markdown快捷键收藏)
  • 【前端必看】极大提高开发效率的网页 JS 调试技巧
  • VMware虚拟机安装Linux教程
  • 统计软件与数据分析—Lesson2
  • 【玩转c++】vector讲解和模拟底层实现
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • Git的一些常用操作
  • Hibernate【inverse和cascade属性】知识要点
  • Mithril.js 入门介绍
  • PHP CLI应用的调试原理
  • SQLServer之索引简介
  • 对象管理器(defineProperty)学习笔记
  • 老板让我十分钟上手nx-admin
  • 前端代码风格自动化系列(二)之Commitlint
  • 前端面试之CSS3新特性
  • 前嗅ForeSpider采集配置界面介绍
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 数组大概知多少
  • 网络应用优化——时延与带宽
  • 我有几个粽子,和一个故事
  • 一加3T解锁OEM、刷入TWRP、第三方ROM以及ROOT
  • 3月27日云栖精选夜读 | 从 “城市大脑”实践,瞭望未来城市源起 ...
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • #Linux(Source Insight安装及工程建立)
  • #Lua:Lua调用C++生成的DLL库
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • (¥1011)-(一千零一拾一元整)输出
  • (zhuan) 一些RL的文献(及笔记)
  • (附源码)ssm跨平台教学系统 毕业设计 280843
  • (免费分享)基于springboot,vue疗养中心管理系统
  • (七)理解angular中的module和injector,即依赖注入
  • (学习日记)2024.03.12:UCOSIII第十四节:时基列表
  • (转)Linq学习笔记
  • (转)nsfocus-绿盟科技笔试题目
  • .net framework 4.0中如何 输出 form 的name属性。
  • .net refrector
  • .NET 中让 Task 支持带超时的异步等待
  • .NET/C# 推荐一个我设计的缓存类型(适合缓存反射等耗性能的操作,附用法)
  • /bin/bash^M: bad interpreter: No such file ordirectory
  • [2018/11/18] Java数据结构(2) 简单排序 冒泡排序 选择排序 插入排序
  • [AIGC] 开源流程引擎哪个好,如何选型?
  • [Android Studio] 开发Java 程序
  • [Android]一个简单使用Handler做Timer的例子
  • [BUUCTF NewStarCTF 2023 公开赛道] week4 crypto/pwn
  • [C++] new和delete