一文看懂Linux内核页缓存(Page Cache)

news2024/11/20 22:37:59

我们知道文件一般存放在硬盘(机械硬盘或固态硬盘)中,CPU 并不能直接访问硬盘中的数据,而是需要先将硬盘中的数据读入到内存中,然后才能被 CPU 访问。

由于读写硬盘的速度比读写内存要慢很多(DDR4 内存读写速度

什么是页缓存

为了提升对文件的读写效率,Linux 内核会以页大小(4KB)为单位,将文件划分为多数据块。当用户对文件中的某个数据块进行读写操作时,内核首先会申请一个内存页(称为 页缓存)与文件中的数据块进行绑定。如下图所示:

如上图所示,当用户对文件进行读写时,实际上是对文件的 页缓存 进行读写。所以对文件进行读写操作时,会分以下两种情况进行处理:

  • 当从文件中读取数据时,如果要读取的数据所在的页缓存已经存在,那么就直接把页缓存的数据拷贝给用户即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户。
  • 当向文件中写入数据时,如果要写入的数据所在的页缓存已经存在,那么直接把新数据写入到页缓存即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把新数据写入到页缓存中。对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。

资料直通车:最新Linux内核源码资料文档+视频资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

页缓存的实现

前面主要介绍了页缓存的作用和原理,接下来我们将会分析 Linux 内核是怎么实现页缓存机制的。

1. address_space

在 Linux 内核中,使用 file 对象来描述一个被打开的文件,其中有个名为 f_mapping 的字段,定义如下:

struct file {
    ...
    struct address_space *f_mapping;
};

从上面代码可以看出,f_mapping 字段的类型为 address_space 结构,其定义如下:

struct address_space {
    struct inode           *host;      /* owner: inode, block_device */
    struct radix_tree_root page_tree;  /* radix tree of all pages */
    rwlock_t               tree_lock;  /* and rwlock protecting it */
    ...
};

address_space 结构其中的一个作用就是用于存储文件的 页缓存,下面介绍一下各个字段的作用:

  • host:指向当前 address_space 对象所属的文件 inode 对象(每个文件都使用一个 inode 对象表示)。
  • page_tree:用于存储当前文件的 页缓存。
  • tree_lock:用于防止并发访问 page_tree 导致的资源竞争问题。

从 address_space 对象的定义可以看出,文件的 页缓存 使用了 radix树 来存储。

radix树:又名基数树,它使用键值(key-value)对的形式来保存数据,并且可以通过键快速查找到其对应的值。内核以文件读写操作中的数据 偏移量 作为键,以数据偏移量所在的 页缓存 作为值,存储在 address_space 结构的 page_tree 字段中。

下图展示了上述各个结构之间的关系:

如果对 radix树 不太了解,可以简单将其看成可以通过文件偏移量快速找到其所在 页缓存 的结构,有机会我会另外写一篇关于 radix树 的文章。

2. 读文件操作

现在我们来分析一下读取文件数据的过程,用户可以通过调用 read 系统调用来读取文件中的数据,其调用链如下:

read()
└→ sys_read()
   └→ vfs_read()
      └→ do_sync_read()
         └→ generic_file_aio_read()
            └→ do_generic_file_read()
               └→ do_generic_mapping_read()

从上面的调用链可以看出,read 系统调用最终会调用 do_generic_mapping_read 函数来读取文件中的数据,其实现如下:

void
do_generic_mapping_read(struct address_space *mapping,
                        struct file_ra_state *_ra,
                        struct file *filp,
                        loff_t *ppos,
                        read_descriptor_t *desc,
                        read_actor_t actor)
{
    struct inode *inode = mapping->host;
    unsigned long index;
    struct page *cached_page;
    ...

    cached_page = NULL;
    index = *ppos >> PAGE_CACHE_SHIFT;
    ...

    for (;;) {
        struct page *page;
        ...

find_page:
        // 1. 查找文件偏移量所在的页缓存是否存在
        page = find_get_page(mapping, index);
        if (!page) {
            ...
            // 2. 如果页缓存不存在, 那么跳到 no_cached_page 进行处理
            goto no_cached_page; 
        }
        ...

page_ok:
        ...
        // 3. 如果页缓存存在, 那么把页缓存的数据拷贝到用户应用程序的内存中
        ret = actor(desc, page, offset, nr);
        ...
        if (ret == nr && desc->count)
            continue;
        goto out;
        ...

readpage:
        // 4. 从文件读取数据到页缓存中
        error = mapping->a_ops->readpage(filp, page);
        ...
        goto page_ok;
        ...

no_cached_page:
        if (!cached_page) {
            // 5. 申请一个内存页作为页缓存
            cached_page = page_cache_alloc_cold(mapping);
            ...
        }

        // 6. 把新申请的页缓存添加到文件页缓存中
        error = add_to_page_cache_lru(cached_page, mapping, index, GFP_KERNEL);
        ...
        page = cached_page;
        cached_page = NULL;
        goto readpage;
    }

out:
    ...
}

do_generic_mapping_read 函数的实现比较复杂,经过精简后,上面代码只留下最重要的逻辑,可以归纳为以下几个步骤:

  • 通过调用 find_get_page 函数查找要读取的文件偏移量所对应的页缓存是否存在,如果存在就把页缓存中的数据拷贝到应用程序的内存中。
  • 否则调用 page_cache_alloc_cold 函数申请一个空闲的内存页作为新的页缓存,并且通过调用 add_to_page_cache_lru 函数把新申请的页缓存添加到文件页缓存和 LRU 队列中(后面会介绍)。
  • 通过调用 readpage 接口从文件中读取数据到页缓存中,并且把页缓存的数据拷贝到应用程序的内存中。

从上面代码可以看出,当页缓存不存在时会申请一块空闲的内存页作为页缓存,并且通过调用 add_to_page_cache_lru 函数把其添加到文件的页缓存和 LRU 队列中。我们来看看 add_to_page_cache_lru 函数的实现:

int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
                           pgoff_t offset, gfp_t gfp_mask)
{
    // 1. 把页缓存添加到文件页缓存中
    int ret = add_to_page_cache(page, mapping, offset, gfp_mask);
    if (ret == 0)
        lru_cache_add(page); // 2. 把页缓存添加到 LRU 队列中
    return ret;
}

add_to_page_cache_lru 函数主要完成两个工作:

  • 通过调用 add_to_page_cache 函数把页缓存添加到文件页缓存中,也就是添加到 address_space 结构的 page_tree 字段中。
  • 通过调用 lru_cache_add 函数把页缓存添加到 LRU 队列中。LRU 队列用于当系统内存不足时,对页缓存进行清理时使用。

总结

本文主要介绍了 页缓存 的作用和原理,并且介绍了在读取文件数据时对页缓存的处理过程。本文并没有介绍写文件操作对应的页缓存处理和当系统内存不足时怎么释放页缓存,有兴趣的话可以自行阅读相关的代码实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/110360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言期末集训3(大一,超基础,小猫猫大课堂配套练习)——循环结构

更新不易,麻烦多多点赞,欢迎你的提问,感谢你的转发, 最后的最后,关注我,关注我,关注我,你会看到更多有趣的博客哦!!! 喵喵喵,你对我…

[第十二届蓝桥杯/java/算法]F——时间显示

🧑‍🎓个人介绍:大二软件生,现学JAVA、Linux、MySQL、算法 💻博客主页:渡过晚枫渡过晚枫 👓系列专栏:[编程神域 C语言],[java/初学者],[蓝桥杯] &#x1f4d…

五步法搞定BI业务需求梳理

五步法搞定BI业务需求梳理。高手就是把复杂的事情简单化,简单的东西重复做、认真做。 五步法是哪五步 第一, 明确用户。商业智能BI项目的规划一切以用户需求为导向,首先需要明确各层次的需求用户。用户都不能明确,调研的入口就没…

前端工程化与 webpack:webpack 的基本使用

1. 什么是 webpack 概念:webpack 是前端项目工程化的具体解决方案。 主要功能:它提供了友好的前端模块化开发支持,以及代码压缩混淆、处理浏览器端 JavaScript 的兼容性、性 能优化等强大的功能。 好处:让程序员把工作的重心放…

自动控制原理笔记-信号流图-Mason公式-控制系统的传递函数

目录 信号流图与结构图的比较: 掌握结构图与信号流图的转换: Mason增益公式: 式子详解: 使用Mason增益公式步骤: 使用Mason增益公式的例题: ​编辑 控制系统的传递函数 : 开环传递函数…

当红齐天再捧“绽放杯”金奖:全流程算力网络夯实元宇宙“底座”

近日,由工信部主办的第五届“绽放杯”5G应用征集大赛在深圳落幕。本届大赛以“5G赋能数字化,扬帆助力新征程 ”为主题,超7000家单位的2.8万个项目参赛,共享5G赋能实体经济的新技术、新成果。英特尔联合行业合作伙伴再获佳绩。 其…

java ssm热带水果网上商城系统--

目录 第一章 绪论 5 1.1 研究背景 5 1.2系统研究现状 5 1.3 系统实现的功能 6 1.4系统实现的特点 6 1.5 本文的组织结构 6 第二章开发技术与环境配置 7 2.1 Java语言简介 7 2.2JSP技术 8 2.3 MySQL环境配置 8 2.4 MyEclipse环境配置 9 2.5 mysql数据库介绍 9 2.6 B/S架构 9 第三…

Matter理论介绍-通用-1-06:桥接设备-其他功能

【源码、文档、软件、硬件、技术交流、技术支持,入口见文末】 【所有相关IDE、SDK和例程源码均可从群文件免费获取,免安装,解压即用】 持续更新中,欢迎关注! 一、桥接设备的功能更新 桥接设备能够独立与桥接器进行软…

一文带你快速搭建框架(最全MyBatis笔记修改篇)

前言:最近收到小伙伴们的私信说这一篇有点问题,因为我是用Typora搬运笔记没考虑到这个问题,感谢这个小伙伴反映的问题~ 目录 一.概述 1.简介 2.maven构建 二.相关概念 1.Mapper接口 2.ORM思想 三.映射配置文件 1.文件结构 2.映射配…

31. 填充和步幅的代码实现

1. 填充 我们创建一个高度和宽度为3的二维卷积层,并在所有侧边填充1个像素。给定高度和宽度为8的输入,则输出的高度和宽度也是8。 import torch from torch import nn# 为了方便起见,我们定义了一个计算卷积层的函数。 # 此函数初始化卷积层…

互异数

这道题是实验舱举办的"编程一小时"千人马拉松竞赛的第三题! 目录 #C、互异数 题目描述 输入格式 输出格式 输入样例1 输出样例1 输入样例2 输出样例2 数据规模 思路: 1.最大互质数 2.互质数的数量 3.贪心策略 总代码: 总结: 题目链接: #C、互异数 题目…

Typora使用之在腾讯云建立远程图床【多图】

1 安装PicGo PicGo是一款功能非常强大的图床的工具,支持SM.MS、腾讯COS、GitHub图床、七牛云图床、Imgur图床、阿里云OSS等多种图床平台。 下载地址:https://github.com/Molunerfinn/PicGo/releases 一般安装PicGo-Setup-2.3.0-beta.7-ia32.exe。 可以选…

C++模板特化

前言 模板特化对函数和函数都可以使用。它的作用是以某一模板函数或某个模板类为例,大部分情况下需要写的函数或内容是一致的,但是有些特别情况,所以我们需要单独拎出来。 模板参数 模板参数可分为类型形参和非类型形参。 类型形参&#x…

消息中间件(消息队列)

简介 MQ(message queue)消息队列,也叫消息中间件。消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。它是类似于数据库一样需要…

残差网络~

搬来这个 给自己学学啊,残差网络解决了什么,为什么有效 从深度神经网络的两大难题入手,说说残差网络的形式化定义与实现,并深入探讨其作用的机制,并结合文献对残差网络有效性进行了一些可能的解释。 残差网络是深度学习中的一个…

【论文阅读】(2020)Knapsack polytopes: a survey(下)

文章目录六、Valid inequalities, separation and computations 有效的不等式,分离和计算七、Complete linear descriptions of particular knapsack polytopes 特定背包多形体的完整线性描述7.1 Extended formulations7.2 Complete linear descriptions 完整的线性…

JavaFx TreeView TreeItem 设置额外属性

在使用JavaFx 编写GUI程序时,不可避免的需要创建一个树组件,下面是一个简单的树组件的代码。 import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.s…

clickhouse笔记05--快速部署3节点集群

clickhouse笔记05--快速部署3节点集群1 介绍2 方法步骤2.1 部署 zookeeper 集群2.2 拉起 clickhouse 集群2.3 测试集群3 注意事项4 说明1 介绍 clickhouse笔记01–快速部署clickhouse 介绍了如何快速部署单节点clickhouse服务,本文基于该博文继续介绍如何快速部署3…

Java进阶—JUC编程

1、线程和进程 获取CPU核数 /*** author java小豪* version 1.0.0* date 2022/12/15* description 测试*/ public class Test {public static void main(String[] args) {// 获取CPU核数// CPU 密集型,IO密集型System.out.println(Runtime.getRuntime().available…

响应式营销策划文化传媒公司网站模板源码

模板信息: 模板编号:8071 模板编码:UTF8 模板颜色:蓝色 模板分类:设计、广告、文化、影视 适合行业:影视传媒类企业 模板介绍: 本模板自带eyoucms内核,无需再下载eyou系统&#xf…