论好名字的重要性: Linux内核page到folio的变迁

news2024/11/18 7:27:33

一、引子

Once upon a time,Netscape的大拿 Phil Karlton曾经说过:“There are only two hard things in Computer Science: cache invalidation and naming things”,成为程序界流传甚广的名言,可见取名是计算机科学中最难的两件事之一。取名,要用名字恰到好处地描述其想描述的事物,要体现代码注释的最高原则——自注释,这其实一点都不轻松。

取名,一般都是从生僻的变为大众的,这样才能朗朗上口,为人民群众所喜闻乐见,比如陈港生更名为成龙,杨旎奥改名为杨紫,刘福荣改名为刘德华。而内核从page到folio的一次改变,似乎是反其道而行之了。感觉有相当数量的童鞋可能都不见得认识folio这个单词。金山词霸曾经曰过,folio是这个意思:

感觉大概意思,就是通过封面和封底夹在一起的一本书或者一套文献。这个名字,有点古典生僻,它的目标在于解决内核面临的一个纠结状况。至于这个名字叫folio、pageset、superpage还是head_page,其实都没有那么重要了,背后真正重要的是,它要解决什么问题。

二、乱局

下面我们来看folio出现之前,Linux内核的情况。众所周知,在Linux内核中,我们用page来描述一页,这一页通常是4KB。这个世界如果所有人都是4KB的单页,那就简单归一了。但是,在晴朗的天空中,却漂浮中一朵乌云,这朵乌云就是compound page以及由compound page衍生出的hugepage,它们并非总是单页的。

在Linux中,我们并不总是以单一的4KB basepage为单位来获取、映射和释放内存。我们有时候,会把多个4KB复合在一起,进行申请、映射和释放:

1.用户态的透明大页(THP)和HugeTLB大页

我们可能直接用PMD而不是PTE进行映射,把一个2MB的连续物理内存映射到用户态,这样用户态使用它,可以大量减小TLB miss。

在内核中,我们描述内存的单元是page,这个page一般是4KB。但是在THP/HugeTLB的场景下,整个2MB其实是一个整体的概念,这个时候,我们诞生了一种需求:

  • 有时候我们关心的是这个2MB的整体,但是page其实是描述它的4KB的部分,用page来描述整体似乎不太适合;
  • 有时候,我们确实想描述2MB整个整体里面4KB的某个部分,这个时候page似乎比较适合。

2.内核态也可能直接申请和释放compound page

比如一些内核driver会通过__GFP_COMP标记申请order大于0的连续页,形成所谓的compound page(比如2页,4页,8页,16页等组成的复合页,前面的THP/HugeTLB其实也是一种order较大的compound page,由2MB/4KB个页面组成的compound page)。这样的透过__GFP_COMP标记,来向buddy申请内存的driver还是比较多的:

这样的compound page,可以透过内核统一的gc机制进行管理,比如在refcount即将归0的时候,put_page(compound page)可以整体释放compound page。

这显然和前面描述的THP/HugeTLB的情况是一样的,也存在一个在部分和整体两种语义中纠结的问题。

由于多个page构成了一个整体,这些page之间会有关联,我们需要某种方法解决如下的问题:

  1. N个page是否组成了一个整体?
  2. 这些page哪些是head(第0个page)?
  3. 这些page哪些是tail(第1 ~ N - 1个)?
  4. 这些page一共有多少个?
  5. 如果我是一个tail,那我的head是谁?
  6. 这些page如何整体释放?释放的时候需要什么析构动作?

....

在folio出现之前,内核采用如下的方法来解决上述的问题:

  • 在由N个4KB组成的compound page的第0个page结构体(page[0],即head page)上安置一个PG_head标记,逻辑如下:

page->flags |= (1UL << PG_head);

所以,如果传给PageHead() API的是第0个page结构体,由于PG_head为真,这个API返回true。

  • 在由N个4KB组成的compound page的第1~N-1的page结构体(page[1] ~ Page[N-1],即tail page)的compound_head上的最后一位设置1,逻辑如下:

page->compound_head |= 1UL;

而除0位以外的位,则指向真正的head的page即page[0],于是逻辑上,如果传入的是1~N-1这些page结构体,如下两个API分别可以取出head page和判断相关的page是否是tail page(一个compound page除page[0]以外的page):

  • 在page[1]这个结构体的compound_order成员上,放置这个compound page的order,比如如果是连续4个4KB组成的复合页,则page[1].compound_order = 2。所以,如果我们把head传入compound_order这个API,则可以取到compound page的order数:

  • 在page[1]这个结构体的compound_dtor成员上,放置这个compound page的析构函数,此析构函数,在put_page[page[0]]并且refcount即将归0的时候会被执行。不同类型的compound page的析构函数可能会不一样:

整个组织关系如下图:

当然,在HugeTLB和THP的场景下,page[2]还有更多的兼职功能(HugeTLB和THP不可能是只有2页,它们存在2MB/4KB,所以一定存在page[2])。

比如HugeTLB借用page[2]->mapping成员:

而THP借用page[2]的deferred_list:

通过page[0]~page[n-1]中flags、compound_head、compound_dtor成员的特殊串联关系,把这N个page结构体联系在了一起。这产生了一个混乱,很多时候,我们真正想操作的,其实只是compound page的整体,比如get_page()、put_page()、lock_page()、unlock_page()等。于是这样的API里面,广泛地存在这样的compound_head()操作:

就以get_page()为例,传入get_page()的page结构体,其实可能是三种情况:

  1. 就是一个普通的非compound page的4KB page,这个时候,compound_head() API实际还是返回那个page;
  2. 传入的是一个compound page的page[0](也即head page),这个时候,compound_head()返回的还是page[0];
  3. 传入的是compound page的page[1] ~ page[n](也即tail page),这个时候,compound_head()返回的是compound_head - 1,也就是page[0]。

另外,我们一般是用操作一组page的page[0]来操作整个compound page的。

我们能不能把这些含混的语义扯清了呢?比如get_xxx(),这个xxx就是表示我要get一个整体呢?再比如get_yyy()就是表示我要操作一个basepage的yyy呢?另外,get_xxx()这个语义下,函数的参数就不可能是yyy呢?让天堂的归天堂,让尘土的归尘土,丁是丁,卯是卯,不香吗?

get_xxx(struct xxx *x);

get_yyy(struct yyy *y);

而不是

这种混乱的局面,很容易对程序员进行错误的向导,因为程序员写代码的时候,究竟在操作xxx,还是yyy,自己都拎不清了。所以需要在函数体内进行区分操作,相似的问题还存在于lock_page()、unlock_page()之类的API,比如:

其实,优秀的代码都是拎得清的代码,优先的API都是强迫调用者拎清的API。

如果你看最新的内核,则可以看到两组不同的APIs:

void folio_get(struct folio *folio);

void get_page(struct page *page);

void folio_lock(struct folio *folio);

void lock_page(struct page *page);

拎清楚的调用者,如果觉得自己在操作一个整体,它应该调用folio_get、folio_lock,另外,我们也强迫它搞清楚自己的参数是folio而不是page。这对于代码的读者而言,也是赏心悦目的,无需猜测的。因为,代码编写的一个基本原则就是:Don’t make me think!代码的读者并不想猜你究竟是想干xxx还是yyy,你直截了当地告诉我就好。

当我们明确地知道我们在操作一个整体/集合,我们在操作一个folio。那么这个folio和page是什么关系呢?page是folio的一部分。但是,folio结构体的定义是什么呢?在最开始的patch版本里,其实它就是:

所以就数据结构本身而言,folio本质上还是一个page结构体,只是被正名了。folio本质上是一个集合的概念,比如它代表一个班级,但是它的数据结构的字长又和表示班上每个学生的数据结构是一样的。比如你的名字叫黄晓明,你是一个开发组的组长你是个工程师,你这个数据结构,其实和一般的工程师是一样的。但是,有时候,领导说,这个事情让黄晓明这边来干。他其实说的是黄晓明这个小组来干,黄晓明这个时候成为一个集体的概念。最终这个黄晓明其实和其他工程师的数据结构是一样的,但是领导说,让黄晓明干,会比说“让工程师干”要清晰明了的多。逻辑就是这么个逻辑,这体现了内核社区的洁癖,也是代码自注释的原则的体现。

早期的patch长成这样的话:

这多少有点不方便,因为我们为了操作一个folio的flags、LRU、private之类的成员,我们还要先来一次folio->page的操作,比如:

所以正式合入Linux 5.16 内核的folio是长下面这样的,把一些page里面常用字段,提取到了和page同等位置的union里面:

如果你还没看明白呢,也许把folio和page并排列会更明白:

说白了,就是同名成员在同样offset位置的简单数学游戏。这样,类似前面的folio的private的访问,就可以直接是:

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

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

三、破局

由此,我们搞清楚了folio并不是什么新生事物,而是一个有着集合概念的,数据结构与page对等的东西 。这样我们至少破除了folio的神秘感。

我们看看内核里面关于folio的注释:

A folio is a physically, virtually and logically contiguous set of bytes. It is a power-of-two in size, and it is aligned to that same power-of-two. It is at least as large as %PAGE_SIZE. If it is in the page cache, it is at a file offset which is a multiple of that power-of-two. It may be mapped into userspace at an address which is at an arbitrary page offset, but its kernel virtual address is aligned to its size.

其实folio就是物理连续、虚拟连续的2^n次的PAGE_SIZE的一些bytes的集合,当然这个n也是允许是0的。这个时候,有的童鞋就跳出来,为什么单页的集合也可以叫folio?你问这个问题是伤了广大单身群众的心,难道单身自己一个人过就不叫一个家庭了吗?家庭成员数量是一,侬晓得伐?

folio有一点是确定的,它必然不会是一个tail page。从而避免了前面的xxx、yyy的语义混乱(也就是Linux社区说的page结构体 的mess)。

理解理念之后,在实践环节,其实就比较简单了。Folio的开发,分了好多个阶段完成,而第一个阶段的git pull request,就有90个patch:

简单地看到这个数字,可能就有的童鞋直接从入门到放弃了。但是,实际点进去看,真的都是非常简单的替换游戏。比如我们随机点开看一个mm: Add folio_pfn() 【1】这个是求folio的pfn的,它究竟是个什么样子呢?不要太简单好吧:

所以,理解folio,最本质的是理解什么时候用folio,把该用folio的,当成folio用,破除心中的迷雾。

在Linux的层面,至少但是不限于如下这些应该是一个集合:

1.加入lruvec进行内存回收管理的应该是一个集合,它或者是compound page或者就是一个普通的单页“集合”。在内核透明大页THP的场景下,其实THP都是以整体加入lruvec的,将lruvec的相关参数改为folio,可以适应更广泛的情况:THP和非THP进入lruvec。比如,著名的shrink_page_list()函数,现在就叫shrink_folio_list(),从lruvec里面拿到的,也是folio:

2.refcount计数、lock等的应该是一个集合

比如:

哪怕你传的是folio中的某一个page,我lock的还是一个集合:

3.mem_cgroup等的记账charge应该是一个集合;

4.wait writeback、bit等应该是一个集合,比如:

folio_wait_bit(struct folio *folio, int bit_nr);

void folio_wait_writeback(struct folio *folio);

5.与address_space绑定的Page cache的查找、插入、删除等操作应该是一个集合,因为page cache也是可以是THP的。相关代码比如:

6.rmap相关的单元应该是一个集合

鉴于文件页page cache以及进程的匿名页都可以是THP,所以在反向映射等API中,操作的应该也是folio,比如,在 do_anonymous_page(struct vm_fault *vmf)这个经典的匿名页page fault处理函数中,最后rmap和lruvec相关的操作都是folio:

当然,历史的伟大变革不会在一瞬间完成。从page语义向folio语义的转换并非是一蹴而就的,所以可以看看最新的Linux kernel提交,仍然也一些在转义过程中。这些转义发生在文件系统、内存管理等各个领域:

鉴于本质上folio和page数据结构在内存意义上相等,所以基于历史原因短期内难以改掉的代码,如果使用的仍然是page,其实在folio和page之间还是可以比较轻松地转换的。比如最简单就是强行转换:

struct page *p = (struct page *) folio;

这当然是代码的“bad smell”。

由于folio是一个集合语义,所以,在我们关心的是集合的一部分的时候,或者说一个部分是否属于一个compound集合的时候,我们仍然关心的是page,比如,下面的API分别判断page是否是一个tail,page是否属于一个compound:

在比如下面的API,copy一整个folio集合,则需要里面的page一个部分一个部分的copy:

folio_page(folio, n)这个API可以取出一个folio中的第n个page。

四、结语

让集合的是集合,让个体的是个体,条分缕析,是folio设计的根本出发点。面向的问题根源,比怎么解决更加重要。尽管Linus Torvalds也不喜欢folio这个名,但是他认可folio要解决的问题,这比叫folio还是刘麻子更关键。

 

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

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

相关文章

使用SSH远程直连Docker容器

文章目录 1. 下载docker镜像2. 安装ssh服务3. 本地局域网测试4. 安装cpolar5. 配置公网访问地址6. SSH公网远程连接测试7.固定连接公网地址8. SSH固定地址连接测试 转载自cpolar极点云文章&#xff1a;SSH远程直连Docker容器 在某些特殊需求下,我们想ssh直接远程连接docker 容器…

网约车进入饱和期,如祺出行继续蓄力还能泛起多大涟漪?

如祺出行的商业版图又有了新扩张。 6月28日&#xff0c;如祺出行正式开通厦门运营&#xff0c;这是继2022年6月进入长沙后&#xff0c;如祺出行在粤港澳大湾区之外聚焦运营的第二座城市。 而在此前&#xff0c;如祺出行宣布完成8.42亿元B轮融资。据了解&#xff0c;本轮融资为…

react-native-SerialPort 串口插件使用及配置

一、git地址和环境版本 &#xff08;1&#xff09;Git地址&#xff1a;https://github.com/Marcello168/react-native-SerialPort &#xff08;2&#xff09;node版本&#xff1a;14 &#xff08;3&#xff09;react-native版本&#xff1a;0.72 二、环境配置 &#xff08;…

CentOS 安装及基本配置

文章目录 1、root 免密码输入自动登录2、设置 Terminal 计算机终端背景颜色3、关闭关闭锁屏4、You need to be root to perform this command. 1、root 免密码输入自动登录 注&#xff1a;设置免密登录需要使用超级用户权限&#xff0c;即 root 权限 &#xff08;1&#xff0…

数字IC后端学习笔记:等效性检查和ECO

1.形式验证工具 对于某些电路的移植&#xff0c;一般不需要对新电路进行仿真验证&#xff0c;而可以直接通过EDA工具来分析该电路的功能是否与原电路一致&#xff0c;此种验证方法可以大量减少验证时间&#xff0c;提高电路的效率。 等效性检查&#xff08;Equivalence Check&a…

Nuxt重构的填坑之路

我的个人网站是用vuecli写的&#xff0c;SEO不忍直视。于是用Nuxt重构了代码&#xff0c;过程中踩了无数坑&#xff0c;记录如下 一&#xff1a;body样式不生效 正常的body样式设置不能生效&#xff0c;需要在nuxt.config.js中配置 1、设置bodyAttrs的class属性&#xff0c;…

毕业论文设计题目大全(源码+论文)_kaic

1 四足步行机器人设计-机械部分 2 吸扫一体机器人外壳注塑模具设计 3 吸扫一体机器人控制系统设计设计 4 吸扫一体机器人机械结构设计 5 汽车雨刷器机械结构及控制系统软硬件电路设计 6 家庭智能防盗报警系统的设计 7 小区电气智能控制系统的设计 8 果蔬智能售卖…

第66篇:顶级APT后门Sunburst通信流量全过程复盘分析

Part1 前言 大家好&#xff0c;我是ABC_123。前面几周分享了Solarwinds供应链攻击事件的详细攻击流程及Sunburst后门的设计思路&#xff0c;但是多数朋友还是对Sunburst后门的通信过程还是没看明白。本期ABC_123就从流量的角度&#xff0c;把Sunburst后门的通信过程完整地复盘…

压缩点云数据

压缩分辨率参数 LOW_RES_ONLINE_COMPRESSION&#xff1a;低分辨率的在线压缩模式&#xff0c;不保留颜色信息。 MED_RES_ONLINE_COMPRESSION&#xff1a;中等分辨率的在线压缩模式&#xff0c;不保留颜色信息。 HIGH_RES_ONLINE_COMPRESSION&#xff1a;高分辨率的在线压缩模…

Nacos架构与原理 - Nacos-Sync

文章目录 概述官网系统模块架构同步任务管理页面注册中心管理页面使用场景 概述 NacosSync 是⼀个支持多种注册中心的同步组件,基于 Spring boot 开发框架,数据层采用Spring Data JPA &#xff0c;遵循了标准的 JPA 访问规范&#xff0c;支持多种数据源存储,默认使用Hibernate…

c++11 标准模板(STL)(std::basic_ostream)(二)

定义于头文件 <ostream> template< class CharT, class Traits std::char_traits<CharT> > class basic_ostream : virtual public std::basic_ios<CharT, Traits> 类模板 basic_ostream 提供字符流上的高层输出操作。受支持操作包含有格式…

float:right 浮动布局后怎么清除浮动对后面元素的影响

1 用overflow:hidden和overflow:auto 在父元素上 2 用伪元素进行清除浮动 ::after

Ubuntu20.04LTS下安装Intel Realsense D435i驱动与ROS运行D435i节点

Ubuntu20.04LTS下安装Intel Realsense D435i驱动与ROS运行D435i节点 1&#xff1a;RealSense的SDK安装 1.1&#xff1a;更新初始化 sudo apt-get update && sudo apt-get upgrade && sudo apt-get dist-upgrade1.2&#xff1a;注册服务器的公钥 sudo apt-k…

安卓水果店的设计与实现

1.项目概述 随着科学技术和社会经济的不断提高&#xff0c;人们对服务的快捷、便利性要求也越来越高&#xff0c;从而对智能手机上的应用软件提出了更高的要求。一个基于安卓技术的水果系统能够为用户提供一个方便日常操作的便捷点餐功能,它能够满足广大手机用户的对日常水果的…

【Java可执行命令】(七)C头文件创建工具 javah:以Java本机接口(JNI)规范创建C头文件,深入解析创建工具javah ~

Java可执行命令详解之javah 1️⃣ 概念2️⃣ 优势和缺点3️⃣ 使用3.1 语法格式3.1.1 可选参数&#xff1a;-o < file>3.1.2 可选参数&#xff1a;-classpath < path>3.1.3 可选参数&#xff1a;-jni 4️⃣ 应用场景5️⃣ 实现原理6️⃣ 注意事项&#x1f33e; 总结…

win系统安装配置minio笔记

win系统安装配置minio笔记 下载win64版本的minio.exe 可以去minio官网下载&#xff0c;也可以直接在csdn下载&#xff0c;这里提供一个下载地址 https://download.csdn.net/download/ThinkPet/87976200?spm1001.2014.3001.5501配置并启动minio.exe 可以在cmd命令里执行 m…

k8s calico ipip模式详解

一、简介 Calico 是一种容器之间互通的网络方案。在虚拟化平台中&#xff0c;比如 OpenStack、Docker 等都需要实现 workloads 之间互连&#xff0c;但同时也需要对容器做隔离控制。而在多数的虚拟化平台实现中&#xff0c;通常都使用二层隔离技术来实现容器的网络&#xff0c…

毕业喽 ! ——为赋新词强说愁

文章目录 一、引言二、回首六年三、腾讯实习四、遗憾和展望 一、引言 临近毕业&#xff0c;满头思绪&#xff0c;满腔感概&#xff0c;不知从何说起&#xff0c;对离别的不舍、对学生时代即将落幕的留恋和感慨、对即将只身踏入社会的迷茫和不安。果真应验了杜甫老先生的那句话—…

MATLAB散点图绘制

clfx linspace(-3*pi,3*pi,100);y sin(x);color linspace(1,10,length(x));scatter(x,y,25,color,filled);hold onscatter(x0.25*pi,y,100,[0 0 0],*); 大部分时候处理数据还是散点图用的比较多 这里主要是scatter函数&#xff0c;用法是&#xff1a; scatter&#xff08…

华为OD机试真题 Python 实现【机房布局】【2023Q1 200分】,附详细解题思路

目录 一、题目描述二、输入描述三、输出描述四、补充说明五、解题思路六、Python算法源码七、效果展示1、输入2、输出 一、题目描述 小明正在规划一个大型数据中心机房&#xff0c;为了使得机柜上的机器都能正常满负荷工作&#xff0c;需要确保在每个机柜边上至少要有一个电箱…