From e45d6035a73767871dbe2ca27c5cef2e64f29b8a Mon Sep 17 00:00:00 2001 From: gatieme Date: Thu, 1 Sep 2016 22:14:00 +0800 Subject: [PATCH] ... --- .../02-pagetable/01-develop/README.md | 434 ++- .../{01-paging => 02-paging}/README.md | 590 ++-- .../02-pagetable/{02-pte => 03-pte}/README.md | 1609 +++++----- .../README.md | 0 .../{02-memblock => 03-memblock}/README.md | 2686 +++++++++-------- .../README.md | 0 6 files changed, 2668 insertions(+), 2651 deletions(-) rename study/kernel/02-memory/02-pagetable/{01-paging => 02-paging}/README.md (96%) rename study/kernel/02-memory/02-pagetable/{02-pte => 03-pte}/README.md (96%) rename study/kernel/02-memory/03-initialize/{00-initialize => 02-initialize}/README.md (100%) rename study/kernel/02-memory/03-initialize/{02-memblock => 03-memblock}/README.md (96%) rename study/kernel/02-memory/03-initialize/{03-paging_init => 06-paging_init}/README.md (100%) diff --git a/study/kernel/02-memory/02-pagetable/01-develop/README.md b/study/kernel/02-memory/02-pagetable/01-develop/README.md index cf83867..8393a39 100644 --- a/study/kernel/02-memory/02-pagetable/01-develop/README.md +++ b/study/kernel/02-memory/02-pagetable/01-develop/README.md @@ -1,218 +1,216 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -#1 分页机制 -------- - -在虚拟内存中,页表是个映射表的概念, 即从进程能理解的线性地址(linear address)映射到存储器上的物理地址(phisical address). - -很显然,这个页表是需要常驻内存的东西, 以应对频繁的查询映射需要(实际上,现代支持VM的处理器都有一个叫TLB的硬件级页表缓存部件,本文不讨论)。 - - - -##1.1 为什么使用多级页表来完成映射 -------- - - - - -但是为什么要使用多级页表来完成映射呢? - - -用来将虚拟地址映射到物理地址的数据结构称为**页表**, 实现两个地址空间的关联最容易的方式是使用数组, 对虚拟地址空间中的每一页, 都分配一个数组项. 该数组指向与之关联的**页帧**, 但这会引发一个问题, 例如, IA-32体系结构使用4KB大小的页, 在虚拟地址空间为4GB的前提下, 则需要包含100万项的页表. 这个问题在64位体系结构下, 情况会更加糟糕. 而每个进程都需要自身的页表, 这回导致系统中大量的所有内存都用来保存页表. - -设想一个典型的32位的X86系统,它的虚拟内存用户空间(user space)大小为3G, 并且典型的一个页表项(page table entry, pte)大小为4 bytes,每一个页(page)大小为4k bytes。那么这3G空间一共有(3G/4k=)786432个页面,每个页面需要一个pte来保存映射信息,这样一共需要786432个pte! - -如何存储这些信息呢?一个直观的做法是用数组来存储,这样每个页能存储(4k/4=)1K个,这样一共需要(786432/1k=)768个连续的物理页面(phsical page)。而且,这只是一个进程,如果要存放所有N个进程,这个数目还要乘上N! 这是个巨大的数目,哪怕内存能提供这样数量的空间,要找到连续768个连续的物理页面在系统运行一段时间后碎片化的情况下,也是不现实的。 - -为减少页表的大小并容许忽略不需要的区域, 计算机体系结构的涉及会将虚拟地址分成多个部分. 同时虚拟地址空间的大部分们区域都没有使用, 因而页没有关联到页帧, 那么就可以使用功能相同但内存用量少的多的模型: **多级页表** - - - - - - - -但是新的问题来了, 到底采用几级页表合适呢? - - -##1.2 32位系统中2级页表 -------- - - -从80386开始, intel处理器的分页单元是4KB的页, 32位的地址空间被分为3部分 - -| 单元 | 描述 | -|:---:|:----:| -| 页目录表Directory | 最高10位 | -| 页中间表Table | 中间10位 | -| 页内偏移 | 最低12位 | - -即页表被划分为页目录表Directory和页中间表Tabl两个部分 - -此种情况下, 线性地址的转换分为两步完成. - -* 第一步, 基于两级转换表(页目录表和页中间表), 最终查找到地址所在的页帧 - -* 第二步, 基于偏移, 在所在的页帧中查找到对应偏移的物理地址 - -使用这种二级页表可以有效的减少每个进程页表所需的RAM的数量. 如果使用简单的一级页表, 那将需要高达$2^20$个页表, 假设每项4B, 则共需要占用$2^20 * 4B = 4MB$的RAM来表示每个进程的页表. 当然我们并不需要映射所有的线性地址空间(32位机器上线性地址空间为4GB), 内核通常只为进程实际使用的那些虚拟内存区请求页表来减少内存使用量. - -##1.3 64位系统中的分页 -------- - -正常来说, 对于32位的系统两级页表已经足够了, 但是对于64位系统的计算机, 这远远不够. - -首先假设一个大小为4KB的标准页. 因为1KB覆盖$2^10$个地址的范围, 4KB覆盖$2^12$个地址, 所以offset字段需要12位. - -这样线性地址空间就剩下64-12=52位分配给页中间表Table和页目录表Directory. 如果我们现在决定仅仅使用64位中的48位来寻址(这个限制其实已经足够了, 2^48=256TB, 即可达到256TB的寻址空间). 剩下的48-12=36位被分配给Table和Directory字段. 即使我们现在决定位两个字段各预留18位, 那么每个进程的页目录和页表都包含$2^18$个项, 即超过256000个项. - -基于这个原因, 所有64位处理器的硬件分页系统都使用了额外的分页级别. 使用的级别取决于处理器的类型 - -| 平台名称 | 页大小 | 寻址所使用的位数 | 分页级别数 | 线性地址分级 | -|:-----:|:-----:|:-----:|:-----:|-------| -| alpha | 8KB | 43 | 3 | 10 + 10 + 10 + 13 | -| ia64 | 4KB | 39 | 3 | 9 + 9 + 9 + 12 | -| ppc64 | 4KB | 41 | 3 | 10 + 10 + 9 + 12 | -| sh64 | 4KB | 41 | 3 | 10 + 10 + 9 + 12 | -| x86_64 | 4KB | 48 | 4 | 9 + 9 + 9 + 9 + 12 | - - - -##1.4 Linux中的分页 -------- - -层次话的页表用于支持对大地址空间快速, 高效的管理. 因此linux内核堆页表进行了分级. - -前面我们提到过, 对于32位系统中, 两级页表已经足够了. 但是64位修奥更多数量的分页级别. - -为了同时支持适用于32位和64位的系统, Linux采用了通用的分页模型. 在Linux-2.6.10版本中, Linux采用了三级分页模型. 而从2.6.11开始普遍采用了四级分页模型. - -目前的内核的内存管理总是嘉定使用四级页表, 而不管底层处理器是否如此. - -| 单元 | 描述 | -|:---:|:----:| -| 页全局目录 | Page GlobalDirectory | -| 页上级目录 | Page Upper Directory | -| 页中间目录 | Page Middle Directory | -| 页表 | Page Table | -| 页内偏移 | Page Offset | - - -Linux不同于其他的操作系统, 它把计算机分成独立层(体系结构无关)/依赖层(体系结构相关)两个层次. 对于页面的映射和管理也是如此. 页表管理分为两个部分, 第一个部分依赖于体系结构, 第二个部分是体系结构无关的. 所有数据结构几乎都定义在特定体系结构的文件中. 这些数据结构的定义可以在头文件`arch/对应体系/include/asm/page.h` -和`arch/对应体系/include/asm/pgtable.h`中找到. 但是对于AMD64和IA-32已经统一为一个体系结构. 但是在处理页表方面仍然有很多的区别, 因为相关的定义分为两个不同的文件`arch/x86/include/asm/page_32.h` -和`arch/x86/include/asm/page_64.h` -, 类似的也有`pgtable_xx.h` . - - - -#2 页表 -------- - - -Linux内核通过四级页表将虚拟内存空间分为5个部分(4个页表项用于选择页, 1个索引用来表示页内的偏移). 各个体系结构不仅地址长度不同, 而且地址字拆分的方式也不一定相同. 因此内核使用了宏用于将地址分解为各个分量. - - -![linux四级页表](../images/pte.gif) - -其他内容请参照博主的另外两篇博客, 我就不罗嗦了 - -[深入理解计算机系统-之-内存寻址(五)--页式存储管理](http://blog.csdn.net/gatieme/article/details/50651561), 详细讲解了传统的页式存储管理机制 - -[深入理解计算机系统-之-内存寻址(六)--linux中的分页机制](http://blog.csdn.net/gatieme/article/details/50756050), 详细的讲解了Linux内核分页机制的实现机制 - - - -#3 Linux分页机制的演变 -------- - - - -##3.1 Linux的页表实现 -------- - -由于程序存在局部化特征, 这意味着在特定的时间内只有部分内存会被频繁访问,具体点,进程空间中的text段(即程序代码), 堆, 共享库,栈都是固定在进程空间的某个特定部分,这样导致进程空间其实是非常稀疏的, 于是,从硬件层面开始,页表的实现就是采用分级页表的方式,Linux内核当然也这么做。所谓分级简单说就是,把整个进程空间分成区块,区块下面可以再细分,这样在内存中只要常驻某个区块的页表即可,这样可以大量节省内存。 - -##3.2 Linux最初的二级页表 -------- - -Linux最初是在一台i386机器上开发的,这种机器是典型的32位X86架构,支持两级页表 - -一个32位虚拟地址如上图划分。当在进行地址转换时, - -结合在CR3寄存器中存放的页目录(page directory, PGD)的这一页的物理地址,再加上从虚拟地址中抽出高10位叫做页目录表项(内核也称这为pgd)的部分作为偏移, 即定位到可以描述该地址的pgd; - -从该pgd中可以获取可以描述该地址的页表的物理地址,再加上从虚拟地址中抽取中间10位作为偏移, 即定位到可以描述该地址的pte; - -在这个pte中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。 -从上述过程中,可以看出,对虚拟地址的分级解析过程,实际上就是不断深入页表层次,逐渐定位到最终地址的过程,所以这一过程被叫做page talbe walk。 - -至于这种做法为什么能节省内存,举个更简单的例子更容易明白。比如要记录16个球场的使用情况,每张纸能记录4个场地的情况。采用4+4+4+4,共4张纸即可记录,但问题是球场使用得很少,有时候一整张纸记录的4个球场都没人使用。于是,采用4 x 4方案,即把16个球场分为4组,同样每张纸刚好能记录4组情况。这样,使用一张纸A来记录4个分组球场情况,当某个球场在使用时,只要额外使用多一张纸B来记录该球场,同时,在A上记录"某球场由纸B在记录"即可。这样在大部分球场使用很少的情况下,只要很少的纸即困记录,当有球场被使用,有需要再用额外的纸来记录,当不用就擦除。这里一个很重要的前提就是:局部性。 - -##3.3 Linux的三级页表 -------- - -当X86引入物理地址扩展(Pisycal Addrress Extension, PAE)后,可以支持大于4G的物理内存(36位),但虚拟地址依然是32位,原先的页表项不适用,它实际多4 bytes被扩充到8 bytes,这意味着,每一页现在能存放的pte数目从1024变成512了(4k/8)。相应地,页表层级发生了变化,Linus新增加了一个层级,叫做页中间目录(page middle directory, PMD), 变成: - - -| 字段 | 描述 | 位数 | -|:---:|:---:|:---:|:---:| -| cr3 | 指向一个PDPT | crs寄存器存储 | -| PGD | 指向PDPT中4个项中的一个 | 位31~30 | -| PMD | 指向页目录中512项中的一个 | 位29~21 | -| PTE | 指向页表中512项中的一个 | 位20~12 | -| page offset | 4KB页中的偏移 | 位11~0 | - -实际的page table walk依然类似,只不过多了一级。 - -现在就同时存在2级页表和3级页表,在代码管理上肯定不方便。巧妙的是,Linux采取了一种抽象方法:所有架构全部使用3级页表: 即PGD -> PMD -> PTE。那只使用2级页表(如非PAE的X86)怎么办? - -办法是针对使用2级页表的架构,把PMD抽象掉,即虚设一个PMD表项。这样在page table walk过程中,PGD本直接指向PTE的,现在不了,指向一个虚拟的PMD,然后再由PMD指向PTE。这种抽象保持了代码结构的统一。 - -##3.4 Linux的四级页表 -------- - -硬件在发展,3级页表很快又捉襟见肘了,原因是64位CPU出现了, 比如X86_64, 它的硬件是实实在在支持4级页表的。它支持48位的虚拟地址空间[1](不过Linux内核最开始只使用47位)。如下: - - -| 字段 | 描述 | 位数 | -|:---:|:---:|:---:|:---:| -| PML4| 指向一个PDPT | 位47~39 | -| PGD | 指向PDPT中4个项中的一个 | 位38~30 | -| PMD | 指向页目录中512项中的一个 | 位29~21 | -| PTE | 指向页表中512项中的一个 | 位20~12 | -| page offset | 4KB页中的偏移 | 位11~0 - -Linux内核针为使用原来的3级列表(PGD->PMD->PTE),做了折衷。即采用一个唯一的,共享的顶级层次,叫PML4[2]。这个PML4没有编码在地址中,这样就能套用原来的3级列表方案了。不过代价就是,由于只有唯一的PML4, 寻址空间被局限在(239=)512G, 而本来PML4段有9位, 可以支持512个PML4表项的。现在为了使用3级列表方案,只能限制使用一个, 512G的空间很快就又不够用了,解决方案呼之欲出。 - -在2004年10月,当时的X86_64架构代码的维护者Andi Kleen提交了一个叫做4level page tables for Linux的PATCH系列,为Linux内核带来了4级页表的支持。在他的解决方案中,不出意料地,按照X86_64规范,新增了一个PML4的层级, 在这种解决方案中,X86_64拥一个有512条目的PML4, 512条目的PGD, 512条目的PMD, 512条目的PTE。对于仍使用3级目录的架构来说,它们依然拥有一个虚拟的PML4,相关的代码会在编译时被优化掉。 这样,就把Linux内核的3级列表扩充为4级列表。这系列PATCH工作得不错,不久被纳入Andrew Morton的-mm树接受测试。 - -不出意外的话,它将在v2.6.11版本中释出。但是,另一个知名开发者Nick Piggin提出了一些看法,他认为Andi的Patch很不错,不过他认为最好还是把PGD作为第一级目录,把新增加的层次放在中间,并给出了他自己的Patch:alternate 4-level page tables patches。Andi更想保持自己的PATCH, 他认为Nick不过是玩了改名的游戏,而且他的PATCH经过测试很稳定,快被合并到主线了,不宜再折腾。 - -不过Linus却表达了对Nick Piggin的支持,理由是Nick的做法conceptually least intrusive。毕竟作为Linux的扛把子,稳定对于Linus来说意义重大。 - -最终,不意外地,最后Nick Piggin的PATCH在v2.6.11版本中被合并入主线。在这种方案中,4级页表分别是:PGD -> PUD -> PMD -> PTE。 - - - -#4 链接 -------- - -[我对linux内核四级分页理解](http://bbs.csdn.net/topics/390831818) - -[Linux内核4级页表的演进](http://blog.csdn.net/hmsiwtv/article/details/39956981) - -[Linux内存 之 页表](http://biancheng.dnbcw.info/linux/335152.html) - -[内存管理(四) 页表数据结构 ](http://blog.chinaunix.net/uid-21718047-id-3140041.html) - - -[Linux内存管理之我见(二)-页表、页式内存管理机制](http://www.360doc.com/content/11/0804/10/7204565_137844381.shtml) - +Linux分页机制之概述--Linux内存管理(六) +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-09-01 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/details/52384791) | + + + + + +#1 分页机制 +------- + +在虚拟内存中,页表是个映射表的概念, 即从进程能理解的线性地址(linear address)映射到存储器上的物理地址(phisical address). + +很显然,这个页表是需要常驻内存的东西, 以应对频繁的查询映射需要(实际上,现代支持VM的处理器都有一个叫TLB的硬件级页表缓存部件,本文不讨论)。 + + + +##1.1 为什么使用多级页表来完成映射 +------- + + + + +但是为什么要使用多级页表来完成映射呢? + + +用来将虚拟地址映射到物理地址的数据结构称为**页表**, 实现两个地址空间的关联最容易的方式是使用数组, 对虚拟地址空间中的每一页, 都分配一个数组项. 该数组指向与之关联的**页帧**, 但这会引发一个问题, 例如, IA-32体系结构使用4KB大小的页, 在虚拟地址空间为4GB的前提下, 则需要包含100万项的页表. 这个问题在64位体系结构下, 情况会更加糟糕. 而每个进程都需要自身的页表, 这回导致系统中大量的所有内存都用来保存页表. + +设想一个典型的32位的X86系统,它的虚拟内存用户空间(user space)大小为3G, 并且典型的一个页表项(page table entry, pte)大小为4 bytes,每一个页(page)大小为4k bytes。那么这3G空间一共有(3G/4k=)786432个页面,每个页面需要一个pte来保存映射信息,这样一共需要786432个pte! + +如何存储这些信息呢?一个直观的做法是用数组来存储,这样每个页能存储(4k/4=)1K个,这样一共需要(786432/1k=)768个连续的物理页面(phsical page)。而且,这只是一个进程,如果要存放所有N个进程,这个数目还要乘上N! 这是个巨大的数目,哪怕内存能提供这样数量的空间,要找到连续768个连续的物理页面在系统运行一段时间后碎片化的情况下,也是不现实的。 + +为减少页表的大小并容许忽略不需要的区域, 计算机体系结构的涉及会将虚拟地址分成多个部分. 同时虚拟地址空间的大部分们区域都没有使用, 因而页没有关联到页帧, 那么就可以使用功能相同但内存用量少的多的模型: **多级页表** + + + + + + + +但是新的问题来了, 到底采用几级页表合适呢? + + +##1.2 32位系统中2级页表 +------- + + +从80386开始, intel处理器的分页单元是4KB的页, 32位的地址空间被分为3部分 + +| 单元 | 描述 | +|:---:|:----:| +| 页目录表Directory | 最高10位 | +| 页中间表Table | 中间10位 | +| 页内偏移 | 最低12位 | + +即页表被划分为页目录表Directory和页中间表Tabl两个部分 + +此种情况下, 线性地址的转换分为两步完成. + +* 第一步, 基于两级转换表(页目录表和页中间表), 最终查找到地址所在的页帧 + +* 第二步, 基于偏移, 在所在的页帧中查找到对应偏移的物理地址 + +使用这种二级页表可以有效的减少每个进程页表所需的RAM的数量. 如果使用简单的一级页表, 那将需要高达$2^20$个页表, 假设每项4B, 则共需要占用$2^20 * 4B = 4MB$的RAM来表示每个进程的页表. 当然我们并不需要映射所有的线性地址空间(32位机器上线性地址空间为4GB), 内核通常只为进程实际使用的那些虚拟内存区请求页表来减少内存使用量. + +##1.3 64位系统中的分页 +------- + +正常来说, 对于32位的系统两级页表已经足够了, 但是对于64位系统的计算机, 这远远不够. + +首先假设一个大小为4KB的标准页. 因为1KB覆盖$2^10$个地址的范围, 4KB覆盖$2^12$个地址, 所以offset字段需要12位. + +这样线性地址空间就剩下64-12=52位分配给页中间表Table和页目录表Directory. 如果我们现在决定仅仅使用64位中的48位来寻址(这个限制其实已经足够了, 2^48=256TB, 即可达到256TB的寻址空间). 剩下的48-12=36位被分配给Table和Directory字段. 即使我们现在决定位两个字段各预留18位, 那么每个进程的页目录和页表都包含$2^18$个项, 即超过256000个项. + +基于这个原因, 所有64位处理器的硬件分页系统都使用了额外的分页级别. 使用的级别取决于处理器的类型 + +| 平台名称 | 页大小 | 寻址所使用的位数 | 分页级别数 | 线性地址分级 | +|:-----:|:-----:|:-----:|:-----:|-------| +| alpha | 8KB | 43 | 3 | 10 + 10 + 10 + 13 | +| ia64 | 4KB | 39 | 3 | 9 + 9 + 9 + 12 | +| ppc64 | 4KB | 41 | 3 | 10 + 10 + 9 + 12 | +| sh64 | 4KB | 41 | 3 | 10 + 10 + 9 + 12 | +| x86_64 | 4KB | 48 | 4 | 9 + 9 + 9 + 9 + 12 | + + + +##1.4 Linux中的分页 +------- + +层次话的页表用于支持对大地址空间快速, 高效的管理. 因此linux内核堆页表进行了分级. + +前面我们提到过, 对于32位系统中, 两级页表已经足够了. 但是64位修奥更多数量的分页级别. + +为了同时支持适用于32位和64位的系统, Linux采用了通用的分页模型. 在Linux-2.6.10版本中, Linux采用了三级分页模型. 而从2.6.11开始普遍采用了四级分页模型. + +目前的内核的内存管理总是嘉定使用四级页表, 而不管底层处理器是否如此. + +| 单元 | 描述 | +|:---:|:----:| +| 页全局目录 | Page GlobalDirectory | +| 页上级目录 | Page Upper Directory | +| 页中间目录 | Page Middle Directory | +| 页表 | Page Table | +| 页内偏移 | Page Offset | + + +Linux不同于其他的操作系统, 它把计算机分成独立层(体系结构无关)/依赖层(体系结构相关)两个层次. 对于页面的映射和管理也是如此. 页表管理分为两个部分, 第一个部分依赖于体系结构, 第二个部分是体系结构无关的. 所有数据结构几乎都定义在特定体系结构的文件中. 这些数据结构的定义可以在头文件`arch/对应体系/include/asm/page.h`和`arch/对应体系/include/asm/pgtable.h`中找到. 但是对于AMD64和IA-32已经统一为一个体系结构. 但是在处理页表方面仍然有很多的区别, 因为相关的定义分为两个不同的文件`arch/x86/include/asm/page_32.h`和`arch/x86/include/asm/page_64.h`, 类似的也有`pgtable_xx.h` . + + + +#2 页表 +------- + + +Linux内核通过四级页表将虚拟内存空间分为5个部分(4个页表项用于选择页, 1个索引用来表示页内的偏移). 各个体系结构不仅地址长度不同, 而且地址字拆分的方式也不一定相同. 因此内核使用了宏用于将地址分解为各个分量. + + +![linux四级页表](../images/pte.gif) + +其他内容请参照博主的另外两篇博客, 我就不罗嗦了 + +[深入理解计算机系统-之-内存寻址(五)--页式存储管理](http://blog.csdn.net/gatieme/article/details/50651561), 详细讲解了传统的页式存储管理机制 + +[深入理解计算机系统-之-内存寻址(六)--linux中的分页机制](http://blog.csdn.net/gatieme/article/details/50756050), 详细的讲解了Linux内核分页机制的实现机制 + + + +#3 Linux分页机制的演变 +------- + + + +##3.1 Linux的页表实现 +------- + +由于程序存在局部化特征, 这意味着在特定的时间内只有部分内存会被频繁访问,具体点,进程空间中的text段(即程序代码), 堆, 共享库,栈都是固定在进程空间的某个特定部分,这样导致进程空间其实是非常稀疏的, 于是,从硬件层面开始,页表的实现就是采用分级页表的方式,Linux内核当然也这么做。所谓分级简单说就是,把整个进程空间分成区块,区块下面可以再细分,这样在内存中只要常驻某个区块的页表即可,这样可以大量节省内存。 + +##3.2 Linux最初的二级页表 +------- + +Linux最初是在一台i386机器上开发的,这种机器是典型的32位X86架构,支持两级页表 + +一个32位虚拟地址如上图划分。当在进行地址转换时, + +结合在CR3寄存器中存放的页目录(page directory, PGD)的这一页的物理地址,再加上从虚拟地址中抽出高10位叫做页目录表项(内核也称这为pgd)的部分作为偏移, 即定位到可以描述该地址的pgd; + +从该pgd中可以获取可以描述该地址的页表的物理地址,再加上从虚拟地址中抽取中间10位作为偏移, 即定位到可以描述该地址的pte; + +在这个pte中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。 +从上述过程中,可以看出,对虚拟地址的分级解析过程,实际上就是不断深入页表层次,逐渐定位到最终地址的过程,所以这一过程被叫做page talbe walk。 + +至于这种做法为什么能节省内存,举个更简单的例子更容易明白。比如要记录16个球场的使用情况,每张纸能记录4个场地的情况。采用4+4+4+4,共4张纸即可记录,但问题是球场使用得很少,有时候一整张纸记录的4个球场都没人使用。于是,采用4 x 4方案,即把16个球场分为4组,同样每张纸刚好能记录4组情况。这样,使用一张纸A来记录4个分组球场情况,当某个球场在使用时,只要额外使用多一张纸B来记录该球场,同时,在A上记录"某球场由纸B在记录"即可。这样在大部分球场使用很少的情况下,只要很少的纸即困记录,当有球场被使用,有需要再用额外的纸来记录,当不用就擦除。这里一个很重要的前提就是:局部性。 + +##3.3 Linux的三级页表 +------- + +当X86引入物理地址扩展(Pisycal Addrress Extension, PAE)后,可以支持大于4G的物理内存(36位),但虚拟地址依然是32位,原先的页表项不适用,它实际多4 bytes被扩充到8 bytes,这意味着,每一页现在能存放的pte数目从1024变成512了(4k/8)。相应地,页表层级发生了变化,Linus新增加了一个层级,叫做页中间目录(page middle directory, PMD), 变成: + + +| 字段 | 描述 | 位数 | +|:---:|:---:|:---:|:---:| +| cr3 | 指向一个PDPT | crs寄存器存储 | +| PGD | 指向PDPT中4个项中的一个 | 位31~30 | +| PMD | 指向页目录中512项中的一个 | 位29~21 | +| PTE | 指向页表中512项中的一个 | 位20~12 | +| page offset | 4KB页中的偏移 | 位11~0 | + +实际的page table walk依然类似,只不过多了一级。 + +现在就同时存在2级页表和3级页表,在代码管理上肯定不方便。巧妙的是,Linux采取了一种抽象方法:所有架构全部使用3级页表: 即PGD -> PMD -> PTE。那只使用2级页表(如非PAE的X86)怎么办? + +办法是针对使用2级页表的架构,把PMD抽象掉,即虚设一个PMD表项。这样在page table walk过程中,PGD本直接指向PTE的,现在不了,指向一个虚拟的PMD,然后再由PMD指向PTE。这种抽象保持了代码结构的统一。 + +##3.4 Linux的四级页表 +------- + +硬件在发展,3级页表很快又捉襟见肘了,原因是64位CPU出现了, 比如X86_64, 它的硬件是实实在在支持4级页表的。它支持48位的虚拟地址空间[1](不过Linux内核最开始只使用47位)。如下: + + +| 字段 | 描述 | 位数 | +|:---:|:---:|:---:|:---:| +| PML4| 指向一个PDPT | 位47~39 | +| PGD | 指向PDPT中4个项中的一个 | 位38~30 | +| PMD | 指向页目录中512项中的一个 | 位29~21 | +| PTE | 指向页表中512项中的一个 | 位20~12 | +| page offset | 4KB页中的偏移 | 位11~0 + +Linux内核针为使用原来的3级列表(PGD->PMD->PTE),做了折衷。即采用一个唯一的,共享的顶级层次,叫PML4[2]。这个PML4没有编码在地址中,这样就能套用原来的3级列表方案了。不过代价就是,由于只有唯一的PML4, 寻址空间被局限在(239=)512G, 而本来PML4段有9位, 可以支持512个PML4表项的。现在为了使用3级列表方案,只能限制使用一个, 512G的空间很快就又不够用了,解决方案呼之欲出。 + +在2004年10月,当时的X86_64架构代码的维护者Andi Kleen提交了一个叫做4level page tables for Linux的PATCH系列,为Linux内核带来了4级页表的支持。在他的解决方案中,不出意料地,按照X86_64规范,新增了一个PML4的层级, 在这种解决方案中,X86_64拥一个有512条目的PML4, 512条目的PGD, 512条目的PMD, 512条目的PTE。对于仍使用3级目录的架构来说,它们依然拥有一个虚拟的PML4,相关的代码会在编译时被优化掉。 这样,就把Linux内核的3级列表扩充为4级列表。这系列PATCH工作得不错,不久被纳入Andrew Morton的-mm树接受测试。 + +不出意外的话,它将在v2.6.11版本中释出。但是,另一个知名开发者Nick Piggin提出了一些看法,他认为Andi的Patch很不错,不过他认为最好还是把PGD作为第一级目录,把新增加的层次放在中间,并给出了他自己的Patch:alternate 4-level page tables patches。Andi更想保持自己的PATCH, 他认为Nick不过是玩了改名的游戏,而且他的PATCH经过测试很稳定,快被合并到主线了,不宜再折腾。 + +不过Linus却表达了对Nick Piggin的支持,理由是Nick的做法conceptually least intrusive。毕竟作为Linux的扛把子,稳定对于Linus来说意义重大。 + +最终,不意外地,最后Nick Piggin的PATCH在v2.6.11版本中被合并入主线。在这种方案中,4级页表分别是:PGD -> PUD -> PMD -> PTE。 + + + +#4 链接 +------- + +[我对linux内核四级分页理解](http://bbs.csdn.net/topics/390831818) + +[Linux内核4级页表的演进](http://blog.csdn.net/hmsiwtv/article/details/39956981) + +[Linux内存 之 页表](http://biancheng.dnbcw.info/linux/335152.html) + +[内存管理(四) 页表数据结构 ](http://blog.chinaunix.net/uid-21718047-id-3140041.html) + + +[Linux内存管理之我见(二)-页表、页式内存管理机制](http://www.360doc.com/content/11/0804/10/7204565_137844381.shtml) + diff --git a/study/kernel/02-memory/02-pagetable/01-paging/README.md b/study/kernel/02-memory/02-pagetable/02-paging/README.md similarity index 96% rename from study/kernel/02-memory/02-pagetable/01-paging/README.md rename to study/kernel/02-memory/02-pagetable/02-paging/README.md index fbc5604..1d354f2 100644 --- a/study/kernel/02-memory/02-pagetable/01-paging/README.md +++ b/study/kernel/02-memory/02-pagetable/02-paging/README.md @@ -1,294 +1,298 @@ -#1 前景回顾 -------- - - -前面我们讲解了操作系统段式存储管理的主要内容。 - -* 32位,在保护方式下,其能够访问的线性地址空间可达4GB,而且允许几乎不受存储空间限制的虚拟存储器程序。虚拟存储器地址空间能够可达64TB。它还提供了复杂的存储管理和硬件辅助的保护机构和增加了支持多任务操作系统的特别优化的指令。实际上,64TB的虚拟地址空间是有磁盘等外部存储器的支持下实现的。在编写程序是可以放在磁盘存储器上,但在执行时,必须把程序加载到物理存储器中。而存储器管理就是要将46位虚拟地址变换成32位物理地址。 - -* 将程序分成不同的段进行管理,我们编程访问内存地址时,访问的其实是操作系统抽象给我们的虚拟地址,通过段基址:段偏移的方式访问内存虚拟地址,极大了简化了程序员的编程结构。 - -* 通过硬件和操作系统的段式管理机制,实模式下通过段基址左移四位+段内偏移,保护模式下通过段选择子select从段描述符GDT/LDT表中获取到段描述符,然后对基地址和段偏移处理,将虚拟地址转换为线性地址。 - -如果没有采用存储器分页管理机制,那么我们得到的线性地址就直接对应与物理地址,否则,则需要将线性地址转换为物理地址。 - - -从80386开始,所有的80x86处理器都支持分页,它通过设置CR0寄存器的PG标志启用分页。当PG=0时,线性地址就被解释成物理地址。 - ->具体请参见深入[理解计算机系统-之-内存管理(三)--(分段管理机制--段描述符,段选择子,描述符表)](http://blog.csdn.net/gatieme/article/details/50647000) -> ->![这里写图片描述](../images/cpumode.png) - - -#2 页式管理 -------- - -##2.1 分段机制存在的问题 -------- - -分段,是指将程序所需要的内存空间大小的虚拟空间,通过映射机制映射到某个物理地址空间(映射的操作由硬件完成)。分段映射机制解决了之前操作系统存在的两个问题: -* (1)地址空间没有隔离。 -* (2)程序运行的地址不确定。 - -不过分段方法存在一个严重的问题:内存的使用效率低。 - -分段的内存映射单位是整个程序;如果内存不足,被换入换出到磁盘的空间都是整个程序的所需空间,这会造成大量的磁盘访问操作,并且严重降低了运行速度。 - -事实上,很多时候程序运行所需要的数据只是很小的一部分,加入到内存的数据大小可能会很小,并没有必要整体的写入和写出。 - -分页机制解决了上面分段方法所存在的一个内存使用效率问题;其核心思想是系统为程序执行文件中的第x页分配了内存中的第y页,同时y页会添加到进程虚拟空间地址的映射表中(页表),这样程序就可以通过映射访问到内存页y了。 - -##2.2 分页存储的基本内容 -------- - - -分页的基本方法是将地址空间人为地等分成某一个固定大小的页;每一页大小由硬件来决定,或者是由操作系统来决定(如果硬件支持多种大小的页)。目前,以大小为4KB的分页是绝大多数PC操作系统的选择。 -* 逻辑空间等分为页;并从0开始编号 -* 内存空间等分为块,与页面大小相同;从0开始编号 -* 分配内存时,以块为单位将进程中的若干个页分别装入 - -关于进程分页。当我们把进程的虚拟地址空间按页来分割,常用的数据和代码会被装在到内存;暂时没用到的是数据和代码则保存在磁盘中,需要用到的时候,再从磁盘中加载到内存中即可。 -这里需要了解三个概念: - -1. 虚拟页(VP, Virtual Page),虚拟空间中的页; - -2. 物理页(PP, Physical Page),物理内存中的页; - -3. 磁盘页(DP, Disk Page),磁盘中的页。 - -虚拟内存的实现需要硬件的支持,从Virtual Address到Physical Address的映射,通过一个叫MMU(Memory Mangement Unit)的部件来完成 - - -#3 分页机制支持 -------- - -##3.1 硬件分页支持 -------- - - - 分页单元(paging unit)把线性地址转换成物理地址。其中的一个关键任务就是把所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生一个缺页异常。 - -* **页**:为了更高效和更经济的管理内存,线性地址被分为以固定长度为单位的组,成为页。页内部连续的线性地址空间被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和对应的存取权限,而不用指定全部线性地址的存取权限。这里说页,同时指一组线性地址以及这组地址包含的数据 - -* **页框**:分页单元把所有的 RAM 分成固定长度的页框(page frame)(有时叫做物理页)。每一个页框包含一个页(page),也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者只是一个数据块,可以存放在任何页框或磁盘中。 - -* **页表**:把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。 - -#3.2 常规的32bit分页 -------- - -常规4KB分页,32位的线性地址被分成3个域 - -| Directory(目录) | Table(页表) | Offset(偏移量) | -| ------------- |:-------------:| -------------:| -| 最高10位 | 中间10位 | 最低12位 | - - -![这里写图片描述](../images/x86_page.png) - ->线性地址的转换分为两步完成,每一步都基于一种转换表,第一种转换表称为页目录表(page directory),第二种转换表称为页表(page table)。 -> ->为什么需要两级呢? ->目的在于减少每个进程页表所需的 RAM 的数量。如果使用简单的一级页表,将需要高达$ 2^{20}$ 个表项来表示每个进程的页表,即时一个进程并不使用所有的地址,二级模式通过职位进程实际使用的那些虚拟内存区请求页表来减少内存容量。 - -每个活动的进程必须有一个页目录,但是却没有必要马上为所有进程的所有页表都分配 RAM,只有在实际需要一个页表时候才给该页表分配 RAM。 - -页目录项和页表项有同样的结构,每项都包含下面的字段: - -| 字段 | 描述 | -| ----|:----:| -| Present标志 | 如果被置为1,所指的页(或页表)就在主存中;如果该标志为0,则这一页不在主存中,此时这个表项剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址存放在控制寄存器cr2中,并产生14号异常:缺页异常。 | -| | 包含页框物理地址最高20位的字段。由于每一个页框有4KB的容量,它的物理地址必须是4096的倍数,因此物理地址的最低12位总为0.如果这个字段指向一个页目录,相应的页框就含有一个页表;如果它指向一个页表,相应的页框就含有一页数据 | -| Accessed标志 | 每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做 | -| Dirty标志 | 只应用于页表项中。每当对一个页框进行写操作时就设置这个标志。与Accessed标志一样,当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做。| -| Read/Write标志 | 含有页或页表的存取权限(Read/Write或Read)。| -| User/Supervisor标志 | 含有访问页或页表所需的特权级。| -| PCD和PWT标志 | 控制硬件高速缓存处理页或页表的方式。| -| Page Size标志 | 只应用于页目录项。如果设置为1,则页目录项指的是2MB或4MB页框。| -| Global标志 | 只应用于页表项。这个标志是在Pentium Pro中引入的,用来防止常用页从TLB高速缓存中刷新出去。只有在cr4寄存器的页全局启用(Page Global Enable, PGE)标志置位时这个标志才起作用。| - - -正在使用的页目录的物理地址存放在控制寄存器CR3中。 - -了解了以上结构之后,我们看看如何从线性地址转换到物理地址的 : - -* 线性地址中的 Directory 字段决定页目录中的目录项,目录项指向适当的页表 - -* 线性地址中的 Table 字段又决定页表的页表项,页表项含有页所在页框的物理地址 - -* 线性地址中的 Offset 地段决定了页框内的相对位置,由于 offset 为 12 为,所以一页含有 4096 字节的数据 - -> Directory字段和Table字段都是10位长,因此页目录和页表都可以多达1024项。那么一个页目录可以寻址到高达1024*1024*4096=232个存储单元,这和32位地址所期望的一样。 - - - -##3.3 物理地址扩展(PAE)分页机制和扩展分页(PSE) -------- - -处理器所支持的RAM容易受到连接到地址总线上的地址管脚树限制. 早期Intel处理器从80386到Pentium使用32位物理地址. - -从理论上讲, 这样的系统可以使用高达2^32=4GB的RAM, 而实际上, 由于用户进程现行地址空间的需要, 4GB的虚拟地址按照1:3的比例划分给内核虚拟地址空间和进程虚拟地址空间. 则内核只能直接对1GB的线性地址空间进行寻址. - -然而, 大型服务器需要大于4GB的RAM来同时运行数以钱计的进程, 所以必须扩展32位80x86架构所支持的RAM容量. - -Intel通过在它的处理器上把管脚数从32增加到36满足这样的需要, 从Pentinum Pro开始, Intel所有处理器的寻址能力可达到2^36=64GB, 但是只有引入一种新的分页机制才能把32位现行地址转换为36位物理地址才能使用所增加的物理地址. - -从Pentinum Pro处理器开始, Intel引入一种叫做**物理地址扩展(Physical Address Extension, PAE)**的机制. - -从Pentium模型开始,80x86微处理器引入了**扩展分页(externded paging)**,也叫**页大小扩展[Page Size Extension]**, 它允许页框大小为4MB而不是4KB。扩展分页用于把大段连续的线性地址转换成相应的物理地址,在这种情况下,内核可以不用中间页表进行地址转换,从而节省内存并保留TLB项。 - ->但是Linux并没有采用这种机制 - -正如前面所述,通过设置页目录项的Page Size标志启用扩展分页功能。在这种情况下,分页单元把32位线性地址分成两个字段: -Directory:最高10位。 -Offfset:其余22位。 - -扩展分页和正常分页的页目录项基本相同,除了 -* Page Size标志必须被设置。 -* 20位物理地址字段只有最高10位是有意义的。这是因为每一个物理地址都是在以4MB为边界的地方开始的,故这个地址的最低22位为0。 - -通过设置cr4处理器寄存器的PSE标志能使扩展分页与常规分页共存 - - - -Intel为了支持PAE改变了分页机制 - -* 64GB的RAM被分成了2^24个页框, 页表项的物理地址字段从20位扩展到了24位. 因为PAE页表项必须包含12个标志位和24个物理地址位, 总数之和为36, 页表项大小从32位扩展到了64位, 结果, 一个4KB的页表项包含512个表项而不是1024个表项 - -* 引入一个页目录指针表(Page Directory Pointer Table, PDPT)的页表新级别, 它由4个64位表项组成. - -* cr3控制寄存器包含一个27位的页目录指针表(PDPT)基地址字段. 因为PDPT存放在RAM的前4GB中, 并在32字节(2^5)的倍数上对其, 因此27位足以表示这种表的基地址 - -* 当把线性地址映射到4KB的页时(页目录项中的PS标准清0), 32位线性地址将按照如下方式解释 - -| 字段 | 描述 | 位数 | -|:---:|:---:|:---:|:---:| -| cr3 | 指向一个PDPT | crs寄存器存储 | -| PGD | 指向PDPT中4个项中的一个 | 位31~30 | -| PMD | 指向页目录中512项中的一个 | 位29~21 | -| PTE | 指向页表中512项中的一个 | 位20~12 | -| page offset | 4KB页中的偏移 | 位11~0 | - -* 当把现行地址映射到2MB的页时(页目录项中的PS标志置为1), 32位线性地址按照如下方式解释 - -| 字段 | 描述 | 位数 | -|:---:|:---:|:---:|:---:| -| cr3 | 指向一个PDPT | crs寄存器存储 | -| PGD | 指向PDPT中4个项中的一个 | 位31~30 | -| PMD | 指向页目录中512项中的一个 | 位29~21 | -| page offset| 2MB页中的偏移 | 位20~0 | - -总之, 一旦cr3被设置, 就可能寻址高达4GB RAM, 如果我们期望堆更多的RAM进行寻址, 就必须在cr3中放置一个新值, 或改变PDPT的内容. - -但是PAE的主要问题是线性地址仍然是32位长, 这就需要内核黑客用同一线性地址映射不同的RAM区. 很显然, PAE并没有扩大进程的线性地址空间, 因为它只处理物理地址. 此外, 只有内核能够修改进程的页表, 所以在用户态下运行的程序不可能使用大于4GB的物理地址空间. 另一方面, PAE允许内核使用容量高达64GB的RAM, 从而显著的增加系统中的进程数目 - - -##3.4 64位系统中的分页 -------- - - -32位处理器普遍采用两级分页。然而两级分页并不适用于采用64位系统的计算机。 - -原因如下 : - -首先假设一个大小为4KB的标准页,4KB覆盖2^12个地址,所以offset字段是12位。如果我们现在决定仅仅使用64位中的48位来寻址(这个限制仍然能是我们自在地拥有256TB的寻址空间!),剩下的48-12=36位被分配给Table和Directory字段。如果我们决定为两个字段个预留18位,那么每个进程的页目录和页表都含有2^18个项,即超过256000个项。 - -由于这个原因,所有64位处理器的硬件分页系统都使用了额外的分页级别。使用的级别数量取决于处理器的类型。 - -| 平台名称 | 页大小 | 寻址使用位数 | 分页级别 | 线性地址分级| -| --- |:----:| -----:|:----:|:-----:| -| alpha | 8KB | 43 | 3 | 10+10+10+13 | -| ia64 | 4KB | 39 | 3 | 9+9+9+12 | -| ppc64 | 4KB | 41 | 3 | 10+10+9+12 | -| x86_64 | 4KB | 48 | 4 | 9+9+9+9+12 | - -注:ia64是intel的一门高端技术,不与x86_64系统兼容 -IA-32e Paging机制下线性地址映射到4KB的页 - - - -#3.5 硬件保护方案 - -与页和页表相关的特权级只有两个,因为特权由前面“常规分页”一节中所提到的User/Supervisor标志所控制。若这个标志为0,只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址;若该标志为1,则总能对页寻址。 - -此外,与段的3种存取权限(读,写,执行)不同的是,页的存取权限只有两种(读,写)。如果页目录项或页表项的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。 - - - -#4 总结 -------- - -80386 使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。 - -##4.1 为什么使用多级页表 -------- - -假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。 - -##4.2 x86_32两级页表结构 -------- - - -两级表结构的第一级称为页目录,存储在一个4K字节的页面中。页目录表共有1K个表项,每个表项为4个字节,并指向第二级表。线性地址的最高10位(即位31~位32)用来产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个表。 - -两级表结构的第二级称为页表,也刚好存储在一个4K字节的页面中,包含1K个字节的表项,每个表项包含一个页的物理基地址。第二级页表由线性地址的中间10 位(即位21~位12)进行索引,以获得包含页的物理地址的页表项,这个物理地址的高20位与线性地址的低12位形成了最后的物理地址,也就是页转化过程输出的物理地址。 - -![两级页表结构](../images/virtual_to_physical.png) - -![页目录项结构](../images/page_directory.png) - -* 第31~12位是20位页表地址,由于页表地址的低12位总为0,所以用高20位指出32位页表地址就可以了。因此,一个页目录最多包含1024个页表地址。 - -* 第0位是存在位,如果P=1,表示页表地址指向的该页在内存中,如果P=0,表示不在内存中。 - -* 第1位是读/写位,第2位是用户/管理员位,这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护。 - -* 第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式 - -* 第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。 - -* 第5位是访问位,当对页目录项进行访问时,A位=1。 - -* 第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。 - -* 第9~11位由操作系统专用,Linux也没有做特殊之用。 - - -![页面项结构](../images/page.png) - - -80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用。 - -第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1。 - -4GB的内存只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,内存一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB。 - -##4.3 线性地址到物理地址的转换 -------- - - -![32位线性地址到物理地址的转换](../images/virtual_to_physical_32.png) - -* 1.CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。 - -* 2.从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。 - -* 3.将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址。 - -##4.4 扩展分页 -------- - -从奔腾处理器开始,Intel微处理器引进了扩展分页,它允许页的大小为4MB。 - -![这里写图片描述](../images/pae.png) - -在扩展分页的情况下,分页机制把32位线性地址分成两个域:最高10位的目录域和其余22位的偏移量。 - -##4.5 页面高速缓存 -------- - - -![这里写图片描述](../images/page_cache.png) - - +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-09-01 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/details/52384791) | + +#1 前景回顾 +------- + + +前面我们讲解了操作系统段式存储管理的主要内容。 + +* 32位,在保护方式下,其能够访问的线性地址空间可达4GB,而且允许几乎不受存储空间限制的虚拟存储器程序。虚拟存储器地址空间能够可达64TB。它还提供了复杂的存储管理和硬件辅助的保护机构和增加了支持多任务操作系统的特别优化的指令。实际上,64TB的虚拟地址空间是有磁盘等外部存储器的支持下实现的。在编写程序是可以放在磁盘存储器上,但在执行时,必须把程序加载到物理存储器中。而存储器管理就是要将46位虚拟地址变换成32位物理地址。 + +* 将程序分成不同的段进行管理,我们编程访问内存地址时,访问的其实是操作系统抽象给我们的虚拟地址,通过段基址:段偏移的方式访问内存虚拟地址,极大了简化了程序员的编程结构。 + +* 通过硬件和操作系统的段式管理机制,实模式下通过段基址左移四位+段内偏移,保护模式下通过段选择子select从段描述符GDT/LDT表中获取到段描述符,然后对基地址和段偏移处理,将虚拟地址转换为线性地址。 + +如果没有采用存储器分页管理机制,那么我们得到的线性地址就直接对应与物理地址,否则,则需要将线性地址转换为物理地址。 + + +从80386开始,所有的80x86处理器都支持分页,它通过设置CR0寄存器的PG标志启用分页。当PG=0时,线性地址就被解释成物理地址。 + +>具体请参见深入[理解计算机系统-之-内存管理(三)--(分段管理机制--段描述符,段选择子,描述符表)](http://blog.csdn.net/gatieme/article/details/50647000) +> +>![这里写图片描述](../images/cpumode.png) + + +#2 页式管理 +------- + +##2.1 分段机制存在的问题 +------- + +分段,是指将程序所需要的内存空间大小的虚拟空间,通过映射机制映射到某个物理地址空间(映射的操作由硬件完成)。分段映射机制解决了之前操作系统存在的两个问题: +* (1)地址空间没有隔离。 +* (2)程序运行的地址不确定。 + +不过分段方法存在一个严重的问题:内存的使用效率低。 + +分段的内存映射单位是整个程序;如果内存不足,被换入换出到磁盘的空间都是整个程序的所需空间,这会造成大量的磁盘访问操作,并且严重降低了运行速度。 + +事实上,很多时候程序运行所需要的数据只是很小的一部分,加入到内存的数据大小可能会很小,并没有必要整体的写入和写出。 + +分页机制解决了上面分段方法所存在的一个内存使用效率问题;其核心思想是系统为程序执行文件中的第x页分配了内存中的第y页,同时y页会添加到进程虚拟空间地址的映射表中(页表),这样程序就可以通过映射访问到内存页y了。 + +##2.2 分页存储的基本内容 +------- + + +分页的基本方法是将地址空间人为地等分成某一个固定大小的页;每一页大小由硬件来决定,或者是由操作系统来决定(如果硬件支持多种大小的页)。目前,以大小为4KB的分页是绝大多数PC操作系统的选择。 +* 逻辑空间等分为页;并从0开始编号 +* 内存空间等分为块,与页面大小相同;从0开始编号 +* 分配内存时,以块为单位将进程中的若干个页分别装入 + +关于进程分页。当我们把进程的虚拟地址空间按页来分割,常用的数据和代码会被装在到内存;暂时没用到的是数据和代码则保存在磁盘中,需要用到的时候,再从磁盘中加载到内存中即可。 +这里需要了解三个概念: + +1. 虚拟页(VP, Virtual Page),虚拟空间中的页; + +2. 物理页(PP, Physical Page),物理内存中的页; + +3. 磁盘页(DP, Disk Page),磁盘中的页。 + +虚拟内存的实现需要硬件的支持,从Virtual Address到Physical Address的映射,通过一个叫MMU(Memory Mangement Unit)的部件来完成 + + +#3 分页机制支持 +------- + +##3.1 硬件分页支持 +------- + + + 分页单元(paging unit)把线性地址转换成物理地址。其中的一个关键任务就是把所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生一个缺页异常。 + +* **页**:为了更高效和更经济的管理内存,线性地址被分为以固定长度为单位的组,成为页。页内部连续的线性地址空间被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和对应的存取权限,而不用指定全部线性地址的存取权限。这里说页,同时指一组线性地址以及这组地址包含的数据 + +* **页框**:分页单元把所有的 RAM 分成固定长度的页框(page frame)(有时叫做物理页)。每一个页框包含一个页(page),也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者只是一个数据块,可以存放在任何页框或磁盘中。 + +* **页表**:把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。 + +#3.2 常规的32bit分页 +------- + +常规4KB分页,32位的线性地址被分成3个域 + +| Directory(目录) | Table(页表) | Offset(偏移量) | +| ------------- |:-------------:| -------------:| +| 最高10位 | 中间10位 | 最低12位 | + + +![这里写图片描述](../images/x86_page.png) + +>线性地址的转换分为两步完成,每一步都基于一种转换表,第一种转换表称为页目录表(page directory),第二种转换表称为页表(page table)。 +> +>为什么需要两级呢? +>目的在于减少每个进程页表所需的 RAM 的数量。如果使用简单的一级页表,将需要高达$ 2^{20}$ 个表项来表示每个进程的页表,即时一个进程并不使用所有的地址,二级模式通过职位进程实际使用的那些虚拟内存区请求页表来减少内存容量。 + +每个活动的进程必须有一个页目录,但是却没有必要马上为所有进程的所有页表都分配 RAM,只有在实际需要一个页表时候才给该页表分配 RAM。 + +页目录项和页表项有同样的结构,每项都包含下面的字段: + +| 字段 | 描述 | +| ----|:----:| +| Present标志 | 如果被置为1,所指的页(或页表)就在主存中;如果该标志为0,则这一页不在主存中,此时这个表项剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址存放在控制寄存器cr2中,并产生14号异常:缺页异常。 | +| | 包含页框物理地址最高20位的字段。由于每一个页框有4KB的容量,它的物理地址必须是4096的倍数,因此物理地址的最低12位总为0.如果这个字段指向一个页目录,相应的页框就含有一个页表;如果它指向一个页表,相应的页框就含有一页数据 | +| Accessed标志 | 每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做 | +| Dirty标志 | 只应用于页表项中。每当对一个页框进行写操作时就设置这个标志。与Accessed标志一样,当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做。| +| Read/Write标志 | 含有页或页表的存取权限(Read/Write或Read)。| +| User/Supervisor标志 | 含有访问页或页表所需的特权级。| +| PCD和PWT标志 | 控制硬件高速缓存处理页或页表的方式。| +| Page Size标志 | 只应用于页目录项。如果设置为1,则页目录项指的是2MB或4MB页框。| +| Global标志 | 只应用于页表项。这个标志是在Pentium Pro中引入的,用来防止常用页从TLB高速缓存中刷新出去。只有在cr4寄存器的页全局启用(Page Global Enable, PGE)标志置位时这个标志才起作用。| + + +正在使用的页目录的物理地址存放在控制寄存器CR3中。 + +了解了以上结构之后,我们看看如何从线性地址转换到物理地址的 : + +* 线性地址中的 Directory 字段决定页目录中的目录项,目录项指向适当的页表 + +* 线性地址中的 Table 字段又决定页表的页表项,页表项含有页所在页框的物理地址 + +* 线性地址中的 Offset 地段决定了页框内的相对位置,由于 offset 为 12 为,所以一页含有 4096 字节的数据 + +> Directory字段和Table字段都是10位长,因此页目录和页表都可以多达1024项。那么一个页目录可以寻址到高达1024*1024*4096=232个存储单元,这和32位地址所期望的一样。 + + + +##3.3 物理地址扩展(PAE)分页机制和扩展分页(PSE) +------- + +处理器所支持的RAM容易受到连接到地址总线上的地址管脚树限制. 早期Intel处理器从80386到Pentium使用32位物理地址. + +从理论上讲, 这样的系统可以使用高达2^32=4GB的RAM, 而实际上, 由于用户进程现行地址空间的需要, 4GB的虚拟地址按照1:3的比例划分给内核虚拟地址空间和进程虚拟地址空间. 则内核只能直接对1GB的线性地址空间进行寻址. + +然而, 大型服务器需要大于4GB的RAM来同时运行数以钱计的进程, 所以必须扩展32位80x86架构所支持的RAM容量. + +Intel通过在它的处理器上把管脚数从32增加到36满足这样的需要, 从Pentinum Pro开始, Intel所有处理器的寻址能力可达到2^36=64GB, 但是只有引入一种新的分页机制才能把32位现行地址转换为36位物理地址才能使用所增加的物理地址. + +从Pentinum Pro处理器开始, Intel引入一种叫做**物理地址扩展(Physical Address Extension, PAE)**的机制. + +从Pentium模型开始,80x86微处理器引入了**扩展分页(externded paging)**,也叫**页大小扩展[Page Size Extension]**, 它允许页框大小为4MB而不是4KB。扩展分页用于把大段连续的线性地址转换成相应的物理地址,在这种情况下,内核可以不用中间页表进行地址转换,从而节省内存并保留TLB项。 + +>但是Linux并没有采用这种机制 + +正如前面所述,通过设置页目录项的Page Size标志启用扩展分页功能。在这种情况下,分页单元把32位线性地址分成两个字段: +Directory:最高10位。 +Offfset:其余22位。 + +扩展分页和正常分页的页目录项基本相同,除了 +* Page Size标志必须被设置。 +* 20位物理地址字段只有最高10位是有意义的。这是因为每一个物理地址都是在以4MB为边界的地方开始的,故这个地址的最低22位为0。 + +通过设置cr4处理器寄存器的PSE标志能使扩展分页与常规分页共存 + + + +Intel为了支持PAE改变了分页机制 + +* 64GB的RAM被分成了2^24个页框, 页表项的物理地址字段从20位扩展到了24位. 因为PAE页表项必须包含12个标志位和24个物理地址位, 总数之和为36, 页表项大小从32位扩展到了64位, 结果, 一个4KB的页表项包含512个表项而不是1024个表项 + +* 引入一个页目录指针表(Page Directory Pointer Table, PDPT)的页表新级别, 它由4个64位表项组成. + +* cr3控制寄存器包含一个27位的页目录指针表(PDPT)基地址字段. 因为PDPT存放在RAM的前4GB中, 并在32字节(2^5)的倍数上对其, 因此27位足以表示这种表的基地址 + +* 当把线性地址映射到4KB的页时(页目录项中的PS标准清0), 32位线性地址将按照如下方式解释 + +| 字段 | 描述 | 位数 | +|:---:|:---:|:---:|:---:| +| cr3 | 指向一个PDPT | crs寄存器存储 | +| PGD | 指向PDPT中4个项中的一个 | 位31~30 | +| PMD | 指向页目录中512项中的一个 | 位29~21 | +| PTE | 指向页表中512项中的一个 | 位20~12 | +| page offset | 4KB页中的偏移 | 位11~0 | + +* 当把现行地址映射到2MB的页时(页目录项中的PS标志置为1), 32位线性地址按照如下方式解释 + +| 字段 | 描述 | 位数 | +|:---:|:---:|:---:|:---:| +| cr3 | 指向一个PDPT | crs寄存器存储 | +| PGD | 指向PDPT中4个项中的一个 | 位31~30 | +| PMD | 指向页目录中512项中的一个 | 位29~21 | +| page offset| 2MB页中的偏移 | 位20~0 | + +总之, 一旦cr3被设置, 就可能寻址高达4GB RAM, 如果我们期望堆更多的RAM进行寻址, 就必须在cr3中放置一个新值, 或改变PDPT的内容. + +但是PAE的主要问题是线性地址仍然是32位长, 这就需要内核黑客用同一线性地址映射不同的RAM区. 很显然, PAE并没有扩大进程的线性地址空间, 因为它只处理物理地址. 此外, 只有内核能够修改进程的页表, 所以在用户态下运行的程序不可能使用大于4GB的物理地址空间. 另一方面, PAE允许内核使用容量高达64GB的RAM, 从而显著的增加系统中的进程数目 + + +##3.4 64位系统中的分页 +------- + + +32位处理器普遍采用两级分页。然而两级分页并不适用于采用64位系统的计算机。 + +原因如下 : + +首先假设一个大小为4KB的标准页,4KB覆盖2^12个地址,所以offset字段是12位。如果我们现在决定仅仅使用64位中的48位来寻址(这个限制仍然能是我们自在地拥有256TB的寻址空间!),剩下的48-12=36位被分配给Table和Directory字段。如果我们决定为两个字段个预留18位,那么每个进程的页目录和页表都含有2^18个项,即超过256000个项。 + +由于这个原因,所有64位处理器的硬件分页系统都使用了额外的分页级别。使用的级别数量取决于处理器的类型。 + +| 平台名称 | 页大小 | 寻址使用位数 | 分页级别 | 线性地址分级| +| --- |:----:| -----:|:----:|:-----:| +| alpha | 8KB | 43 | 3 | 10+10+10+13 | +| ia64 | 4KB | 39 | 3 | 9+9+9+12 | +| ppc64 | 4KB | 41 | 3 | 10+10+9+12 | +| x86_64 | 4KB | 48 | 4 | 9+9+9+9+12 | + +注:ia64是intel的一门高端技术,不与x86_64系统兼容 +IA-32e Paging机制下线性地址映射到4KB的页 + + + +#3.5 硬件保护方案 + +与页和页表相关的特权级只有两个,因为特权由前面“常规分页”一节中所提到的User/Supervisor标志所控制。若这个标志为0,只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址;若该标志为1,则总能对页寻址。 + +此外,与段的3种存取权限(读,写,执行)不同的是,页的存取权限只有两种(读,写)。如果页目录项或页表项的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。 + + + +#4 总结 +------- + +80386 使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。 + +##4.1 为什么使用多级页表 +------- + +假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。 + +##4.2 x86_32两级页表结构 +------- + + +两级表结构的第一级称为页目录,存储在一个4K字节的页面中。页目录表共有1K个表项,每个表项为4个字节,并指向第二级表。线性地址的最高10位(即位31~位32)用来产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个表。 + +两级表结构的第二级称为页表,也刚好存储在一个4K字节的页面中,包含1K个字节的表项,每个表项包含一个页的物理基地址。第二级页表由线性地址的中间10 位(即位21~位12)进行索引,以获得包含页的物理地址的页表项,这个物理地址的高20位与线性地址的低12位形成了最后的物理地址,也就是页转化过程输出的物理地址。 + +![两级页表结构](../images/virtual_to_physical.png) + +![页目录项结构](../images/page_directory.png) + +* 第31~12位是20位页表地址,由于页表地址的低12位总为0,所以用高20位指出32位页表地址就可以了。因此,一个页目录最多包含1024个页表地址。 + +* 第0位是存在位,如果P=1,表示页表地址指向的该页在内存中,如果P=0,表示不在内存中。 + +* 第1位是读/写位,第2位是用户/管理员位,这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护。 + +* 第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式 + +* 第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。 + +* 第5位是访问位,当对页目录项进行访问时,A位=1。 + +* 第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。 + +* 第9~11位由操作系统专用,Linux也没有做特殊之用。 + + +![页面项结构](../images/page.png) + + +80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用。 + +第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1。 + +4GB的内存只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,内存一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB。 + +##4.3 线性地址到物理地址的转换 +------- + + +![32位线性地址到物理地址的转换](../images/virtual_to_physical_32.png) + +* 1.CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。 + +* 2.从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。 + +* 3.将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址。 + +##4.4 扩展分页 +------- + +从奔腾处理器开始,Intel微处理器引进了扩展分页,它允许页的大小为4MB。 + +![这里写图片描述](../images/pae.png) + +在扩展分页的情况下,分页机制把32位线性地址分成两个域:最高10位的目录域和其余22位的偏移量。 + +##4.5 页面高速缓存 +------- + + +![这里写图片描述](../images/page_cache.png) + + 由于在分页情况下,每次存储器访问都要存取两级页表,这就大大降低了访问速度。所以,为了提高速度,在386中设置一个最近存取页面的高速缓存硬件机制,它 自动保持32项处理器最近使用的页面地址,因此,可以覆盖128K字节的存储器地址。当进行存储器访问时,先检查要访问的页面是否在高速缓存中,如果在, 就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓存大约有98%的命中率,也就是说每次访问存储器时,只有2%的情况必须访问两级分页机构。这就大大加快了速度。 \ No newline at end of file diff --git a/study/kernel/02-memory/02-pagetable/02-pte/README.md b/study/kernel/02-memory/02-pagetable/03-pte/README.md similarity index 96% rename from study/kernel/02-memory/02-pagetable/02-pte/README.md rename to study/kernel/02-memory/02-pagetable/03-pte/README.md index f4987c5..f2b4520 100644 --- a/study/kernel/02-memory/02-pagetable/02-pte/README.md +++ b/study/kernel/02-memory/02-pagetable/03-pte/README.md @@ -1,803 +1,808 @@ ->[注意] ->如果您当前使用的系统并不是linux,或者您的系统中只有一份linux源码,而您又期待能够查看或者检索不同版本的linux源码 -> ->可以使用 http://lxr.free-electrons.com/ -> ->LXR (Linux Cross Reference)是比较流行的linux源代码查看工具,而这里集成了全版本的linux源码的索引 - - -#1 linux的分页机制 -------- - - -##1.1 四级分页机制 -------- - - -前面我们提到Linux内核仅使用了较少的分段机制,但是却对分页机制的依赖性很强,其使用一种适合32位和64位结构的通用分页模型,该模型使用**四级分页机制**,即 - -* 页全局目录(Page Global Directory) -* 页上级目录(Page Upper Directory) -* 页中间目录(Page Middle Directory) -* 页表(Page Table) - ->* 页全局目录包含若干页上级目录的地址; ->* 页上级目录又依次包含若干页中间目录的地址; ->* 而页中间目录又包含若干页表的地址; ->* 每一个页表项指向一个页框。 ->因此线性地址因此被分成五个部分,而每一部分的大小与具体的计算机体系结构有关。 - - -![Linux四级页表](http://img.blog.csdn.net/20160806144330104) - - -##1.3 不同架构的分页机制 -------- - - ->对于不同的体系结构,Linux采用的四级页表目录的大小有所不同:对于i386而言,仅采用二级页表,即页上层目录和页中层目录长度为0;对于启用PAE的i386,采用了三级页表,即页上层目录长度为0;对于64位体系结构,可以采用三级或四级页表,具体选择由硬件决定。 - -对于没有启用物理地址扩展的32位系统,两级页表已经足够了。从本质上说Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项而实现的。 - -启用了物理地址扩展的32 位系统使用了三级页表。Linux 的页全局目录对应80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。 - -最终,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。 - - -##1.4 为什么linux热衷:分页>分段 -------- - -那么,为什么Linux是如此地热衷使用分页技术而对分段机制表现得那么地冷淡呢,因为Linux的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行: - -* 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。 -* 区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又被装在不同的页框中。这就是虚拟内存机制的基本要素。 - -每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。 - -把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械式的任务。 - -#2 linux中页表处理数据结构 -------- - -##2.1 页表类型定义pgd_t、pmd_t、pud_t和pte_t -------- -Linux分别采用pgd_t、pmd_t、pud_t和pte_t四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种 数据结构本质上都是无符号长整型unsigned long - -Linux为了更严格数据类型检查,将无符号长整型unsigned long分别封装成四种不同的页表项。如果不采用这种方法,那么一个无符号长整型数据可以传入任何一个与四种页表相关的函数或宏中,这将大大降低程序的健壮性。 - ->pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。 - -首先我们查看一下子这些类型是如何定义的 - -###2.1.1 pteval_t,pmdval_t,pudval_t,pgdval_t -------- - - ->参照[arch/x86/include/asm/pgtable_64_types.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable_64_types.h#L227) - -``` -#ifndef __ASSEMBLY__ -#include - -/* - * These are used to make use of C type-checking.. - */ -typedef unsigned long pteval_t; -typedef unsigned long pmdval_t; -typedef unsigned long pudval_t; -typedef unsigned long pgdval_t; -typedef unsigned long pgprotval_t; - -typedef struct { pteval_t pte; } pte_t; - -#endif /* !__ASSEMBLY__ */ -``` -###2.1.2 pgd_t、pmd_t、pud_t和pte_t -------- - - ->参照 [/arch/x86/include/asm/pgtable_types.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable_types.h#L227) - -``` -typedef struct { pgdval_t pgd; } pgd_t; - -static inline pgd_t native_make_pgd(pgdval_t val) -{ - return (pgd_t) { val }; -} - -static inline pgdval_t native_pgd_val(pgd_t pgd) -{ - return pgd.pgd; -} - -static inline pgdval_t pgd_flags(pgd_t pgd) -{ - return native_pgd_val(pgd) & PTE_FLAGS_MASK; -} - -#if CONFIG_PGTABLE_LEVELS > 3 -typedef struct { pudval_t pud; } pud_t; - -static inline pud_t native_make_pud(pmdval_t val) -{ - return (pud_t) { val }; -} - -static inline pudval_t native_pud_val(pud_t pud) -{ - return pud.pud; -} -#else -#include - -static inline pudval_t native_pud_val(pud_t pud) -{ - return native_pgd_val(pud.pgd); -} -#endif - -#if CONFIG_PGTABLE_LEVELS > 2 -typedef struct { pmdval_t pmd; } pmd_t; - -static inline pmd_t native_make_pmd(pmdval_t val) -{ - return (pmd_t) { val }; -} - -static inline pmdval_t native_pmd_val(pmd_t pmd) -{ - return pmd.pmd; -} -#else -#include - -static inline pmdval_t native_pmd_val(pmd_t pmd) -{ - return native_pgd_val(pmd.pud.pgd); -} -#endif - -static inline pudval_t pud_pfn_mask(pud_t pud) -{ - if (native_pud_val(pud) & _PAGE_PSE) - return PHYSICAL_PUD_PAGE_MASK; - else - return PTE_PFN_MASK; -} - -static inline pudval_t pud_flags_mask(pud_t pud) -{ - return ~pud_pfn_mask(pud); -} - -static inline pudval_t pud_flags(pud_t pud) -{ - return native_pud_val(pud) & pud_flags_mask(pud); -} - -static inline pmdval_t pmd_pfn_mask(pmd_t pmd) -{ - if (native_pmd_val(pmd) & _PAGE_PSE) - return PHYSICAL_PMD_PAGE_MASK; - else - return PTE_PFN_MASK; -} - -static inline pmdval_t pmd_flags_mask(pmd_t pmd) -{ - return ~pmd_pfn_mask(pmd); -} - -static inline pmdval_t pmd_flags(pmd_t pmd) -{ - return native_pmd_val(pmd) & pmd_flags_mask(pmd); -} - -static inline pte_t native_make_pte(pteval_t val) -{ - return (pte_t) { .pte = val }; -} - -static inline pteval_t native_pte_val(pte_t pte) -{ - return pte.pte; -} - -static inline pteval_t pte_flags(pte_t pte) -{ - return native_pte_val(pte) & PTE_FLAGS_MASK; -} -``` - - -###2.1.4 xxx_val和__xxx -------- - - ->参照[/arch/x86/include/asm/pgtable.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable.h#L227) - -五个类型转换宏(__ pte、__ pmd、__ pud、__ pgd和__ pgprot)把一个无符号整数转换成所需的类型。 - -另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。 -``` -#define pgd_val(x) native_pgd_val(x) -#define __pgd(x) native_make_pgd(x) - -#ifndef __PAGETABLE_PUD_FOLDED -#define pud_val(x) native_pud_val(x) -#define __pud(x) native_make_pud(x) -#endif - -#ifndef __PAGETABLE_PMD_FOLDED -#define pmd_val(x) native_pmd_val(x) -#define __pmd(x) native_make_pmd(x) -#endif - -#define pte_val(x) native_pte_val(x) -#define __pte(x) native_make_pte(x) -``` - - -这里需要区别指向页表项的指针和页表项所代表的数据。以pgd_t类型为例子,如果已知一个pgd_t类型的指针pgd,那么通过pgd_val(*pgd)即可获得该页表项(也就是一个无符号长整型数据),这里利用了面向对象的思想。 - -##2.2 页表描述宏 -------- - - -参照[arch/x86/include/asm/pgtable_64](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable_64_types.h#L227) -linux中使用下列宏简化了页表处理,对于每一级页表都使用有以下三个关键描述宏: -| 宏字段| 描述 | -| ------------- |:-------------:| -----:| -| XXX_SHIFT| 指定Offset字段的位数 | -| XXX_SIZE| 页的大小 | -| XXX_MASK| 用以屏蔽Offset字段的所有位。 | - -我们的四级页表,对应的宏分别由PAGE,PMD,PUD,PGDIR -| 宏字段前缀| 描述 | -| ------------- |:-------------:| -----:| -| PGDIR | 页全局目录(Page Global Directory) | -| PUD | 页上级目录(Page Upper Directory) | -| PMD | 页中间目录(Page Middle Directory) | -| PAGE | 页表(Page Table) | - -###2.2.1 PAGE宏--页表(Page Table) -------- - -| 字段| 描述 | -| ------------- |:-------------:| -----:| -| PAGE_SHIFT| 指定Offset字段的位数 | -| PAGE_SIZE| 页的大小 | -| PAGE_MASK| 用以屏蔽Offset字段的所有位。 | - -定义如下,在`/arch/x86/include/asm/page_types.h` 文件中 - -``` -/* PAGE_SHIFT determines the page size */ - #define PAGE_SHIFT 12 - #define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT) - #define PAGE_MASK (~(PAGE_SIZE-1)) -``` -当用于80x86处理器时,PAGE_SHIFT返回的值为12。 -由于页内所有地址都必须放在Offset字段, 因此80x86系统的页的大小PAGE_SIZE是$2^{12} =4096$字节。 -PAGE_MASK宏产生的值为0xfffff000,用以屏蔽Offset字段的所有位。 - -###2.2.2 PMD-Page Middle Directory (页目录) -------- - - -| 字段| 描述 | -| ------------- |:-------------:| -----:| -| PMD_SHIFT| 指定线性地址的Offset和Table字段的总位数;换句话说,是页中间目录项可以映射的区域大小的对数 | -| PMD_SIZE| 用于计算由页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小 | -| PMD_MASK| 用于屏蔽Offset字段与Table字段的所有位 | - -**当PAE 被禁用时**,PMD_SHIFT 产生的值为22(来自Offset 的12 位加上来自Table 的10 位), -PMD_SIZE 产生的值为222 或 4 MB, -PMD_MASK产生的值为 0xffc00000。 - -相反,**当PAE被激活时**, -PMD_SHIFT 产生的值为21 (来自Offset的12位加上来自Table的9位), -PMD_SIZE 产生的值为$2^{21}$ 或2 MB -PMD_MASK产生的值为 0xffe00000。 - ->大型页不使用最后一级页表,所以产生大型页尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。 - -###2.2.3 PUD_SHIFT-页上级目录(Page Upper Directory) -------- - - -| 字段| 描述 | -| ------------- |:-------------:| -----:| -| PUD_SHIFT| 确定页上级目录项能映射的区域大小的位数 | -| PUD_SIZE| 用于计算页全局目录中的一个单独表项所能映射的区域大小。 | -| PUD_MASK| 用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位 | - -在80x86处理器上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4MB或2MB。 - -###2.2.4 PGDIR_SHIFT-页全局目录(Page Global Directory) -------- - - -| 字段| 描述 | -| ------------- |:-------------:| -----:| -| PGDIR_SHIFT| 确定页全局页目录项能映射的区域大小的位数 | -| PGDIR_SIZE| 用于计算页全局目录中一个单独表项所能映射区域的大小 | -| PGDIR_MASK| 用于屏蔽Offset, Table,Middle Air及Upper Air的所有位 | - -**当PAE 被禁止时**, -PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同), -PGDIR_SIZE 产生的值为 222 或 4 MB, -PGDIR_MASK 产生的值为 0xffc00000。 - -相反,**当PAE被激活时**, -PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air), -PGDIR_SIZE 产生的值为230 或 1 GB -PGDIR_MASK产生的值为0xc0000000 - -PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD - -用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。 - -##2.3 页表处理函数 -------- - - ->**[注意]** -> 以下内容主要参见 [深入理解linux内核第二章内存寻址中页表处理](http://www.bubuko.com/infodetail-1185213.html) - - -内核还提供了许多宏和函数用于读或修改页表表项: - -* 如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。 - -* 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear( )函数清除一个页表项并返回前一个值。 - -* set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。set_pte_atomic与set_pte作用相同,但是当PAE被激活时它同样能保证64位的值能被原子地写入。 - -* 如果a和b两个页表项指向同一页并且指定相同访问优先级,pte_same(a,b)返回1,否则返回0。 - -* 如果页中间目录项指向一个大型页(2MB或4MB),pmd_large(e)返回1,否则返回0。 - -宏pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这个宏产生的值为1: - -* 页不在主存中(Present标志被清除)。 - -* 页只允许读访问(Read/Write标志被清除)。 - -* Acessed或者Dirty位被清除(对于每个现有的页表,Linux总是 -强制设置这些标志)。 - -pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。 - -如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。 - -前面讲过页表项的Page Size标志对微处理器的分页部件来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其Present和Page Size分别标记为0和1。 - -这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检测到产生异常并不是因为缺页。 - -如果相应表项的Present标志等于1,也就是说,如果对应的页或页表被装载入主存,pmd_present宏产生的值为1。pud_present宏和pgd_present宏产生的值总是1。 - -###2.3.1 查询页表项中任意一个标志的当前值 -------- - - -下表中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其他函数只有在pte_present返回1的时候,才能正常返回页表项中任意一个标志。 -| 函数名称 | 说明 | -| ------------- |:-------------:| -----:| -| pte_user( ) | 读 User/Supervisor 标志 | -| pte_read( ) | 读 User/Supervisor 标志(表示 80x86 处理器上的页不受读的保护)| -| pte_write( ) | 读 Read/Write 标志 | -| pte_exec( ) | 读 User/Supervisor 标志( 80x86 处理器上的页不受代码执行的保护)| -| pte_dirty( ) | 读 Dirty 标志 | -| pte_young( ) | 读 Accessed 标志 | -| pte_file( ) | 读 Dirty 标志(当 Present 标志被清除而 Dirty 标志被设置时,页属于一个非线性磁盘文件映射)| - - -###2.3.2 设置页表项中各标志的值 -------- - - -下表列出的另一组函数用于设置页表项中各标志的值 - -| 函数名称 | 说明 | -| ------------- |:-------------:| -| mk_pte_huge( ) | 设置页表项中的 Page Size 和 Present 标志 | -| pte_wrprotect( ) | 清除 Read/Write 标志 | -| pte_rdprotect( ) | 清除 User/Supervisor 标志 | -| pte_exprotect( ) | 清除 User/Supervisor 标志 | -| pte_mkwrite( ) | 设置 Read/Write 标志 | -| pte_mkread( ) | 设置 User/Supervisor 标志 | -| pte_mkexec( ) | 设置 User/Supervisor 标志 | -| pte_mkclean( ) | 清除 Dirty 标志 | -| pte_mkdirty( ) | 设置 Dirty 标志 | -| pte_mkold( ) | 清除 Accessed 标志(把此页标记为未访问)| -| pte_mkyoung( ) | 设置 Accessed 标志(把此页标记为访问过)| -| pte_modify(p,v) | 把页表项 p 的所有访问权限设置为指定的值 | -| ptep_set_wrprotect() | 与 pte_wrprotect( ) 类似,但作用于指向页表项的指针 | -| ptep_set_access_flags( ) | 如果 Dirty 标志被设置为 1 则将页的访问权设置为指定的值,并调用flush_tlb_page() 函数 | -| ptep_mkdirty() | 与 pte_mkdirty( ) 类似,但作用于指向页表项的指针。| -| ptep_test_and_clear_dirty( ) | 与 pte_mkclean( ) 类似,但作用于指向页表项的指针并返回 Dirty 标志的旧值 | -| ptep_test_and_clear_young( ) | 与 pte_mkold( ) 类似,但作用于指向页表项的指针并返回 Accessed标志的旧值 | - -###2.3.3 宏函数-把一个页地址和一组保护标志组合成页表项,或者执行相反的操作 -------- - - -现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。 -| 宏名称 | 说明 | -| ------------- |:-------------:| -| pgd_index(addr) | 找到线性地址 addr 对应的的目录项在页全局目录中的索引(相对位置)| -| pgd_offset(mm, addr) | 接收内存描述符地址 mm 和线性地址 addr 作为参数。这个宏产生地址addr 在页全局目录中相应表项的线性地址;通过内存描述符 mm 内的一个指针可以找到这个页全局目录 | -| pgd_offset_k(addr) | 产生主内核页全局目录中的某个项的线性地址,该项对应于地址 addr | -| pgd_page(pgd) | 通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址。在两级或三级分页系统中,该宏等价于 pud_page() ,后者应用于页上级目录项 | -| pud_offset(pgd, addr) | 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址 | -| pud_page(pud) | 通过页上级目录项 pud 产生相应的页中间目录的线性地址。在两级分页系统中,该宏等价于 pmd_page() ,后者应用于页中间目录项 | -| pmd_index(addr) | 产生线性地址 addr 在页中间目录中所对应目录项的索引(相对位置)| -| pmd_offset(pud, addr) | 接收指向页上级目录项的指针 pud 和线性地址 addr 作为参数。这个宏产生目录项 addr 在页中间目录中的偏移地址。在两级或三级分页系统中,它产生 pud ,即页全局目录项的地址 | -| pmd_page(pmd) | 通过页中间目录项 pmd 产生相应页表的页描述符地址。在两级或三级分页系统中, pmd 实际上是页全局目录中的一项 | -| mk_pte(p,prot) | 接收页描述符地址 p 和一组访问权限 prot 作为参数,并创建相应的页表项 | -| pte_index(addr) | 产生线性地址 addr 对应的表项在页表中的索引(相对位置)| -| pte_offset_kernel(dir,addr) | 线性地址 addr 在页中间目录 dir 中有一个对应的项,该宏就产生这个对应项,即页表的线性地址。另外,该宏只在主内核页表上使用 | -| pte_offset_map(dir, addr) | 接收指向一个页中间目录项的指针 dir 和线性地址 addr 作为参数,它产生与线性地址 addr 相对应的页表项的线性地址。如果页表被保存在高端存储器中,那么内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它们使用不同的临时内核映射 | -| pte_page( x ) | 返回页表项 x 所引用页的描述符地址 | -| pte_to_pgoff( pte ) | 从一个页表项的 pte 字段内容中提取出文件偏移量,这个偏移量对应着一个非线性文件内存映射所在的页 | -| pgoff_to_pte(offset ) | 为非线性文件内存映射所在的页创建对应页表项的内容| - - - -###2.3.4 简化页表项的创建和撤消 -------- - - -下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。 - -如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。 - -| 函数名称 | 说明 | -| ------------- |:-------------:| -| pgd_alloc( mm ) | 分配一个新的页全局目录。如果 PAE 被激活,它还分配三个对应用户态线性地址的子页中间目录。参数 mm( 内存描述符的地址 )在 80x86 构架上被忽略 | -| pgd_free( pgd) | 释放页全局目录中地址为 pgd 的项。如果 PAE 被激活,它还将释放用户态线性地址对应的三个页中间目录 | -| pud_alloc(mm, pgd, addr) | 在两级或三级分页系统下,这个函数什么也不做:它仅仅返回页全局目录项 pgd 的线性地址 | -| pud_free(x) | 在两级或三级分页系统下,这个宏什么也不做 | -| pmd_alloc(mm, pud, addr) | 定义这个函数以使普通三级分页系统可以为线性地址 addr 分配一个新的页中间目录。如果 PAE 未被激活,这个函数只是返回输入参数 pud 的值,也就是说,返回页全局目录中目录项的地址。如果 PAE 被激活,该函数返回线性地址 addr 对应的页中间目录项的线性地址。参数 mm 被忽略 | -| pmd_free(x) | 该函数什么也不做,因为页中间目录的分配和释放是随同它们的父全局目录一同进行的 | -| pte_alloc_map(mm, pmd, addr) | 接收页中间目录项的地址 pmd 和线性地址 addr 作为参数,并返回与 addr 对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表。如果分配了一个新页表, addr 对应的项就被创建,同时 User/Supervisor 标志被设置为 1 。如果页表被保存在高端内存,则内核建立一个临时内核映射,并用 pte_unmap 对它进行释放 | -| pte_alloc_kernel(mm, pmd, addr) | 如果与地址 addr 相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 addr 相关的页表项的线性地址。该函数仅被主内核页表使用 | -| pte_free(pte) | 释放与页描述符指针 pte 相关的页表 | -| pte_free_kernel(pte) | 等价于 pte_free( ) ,但由主内核页表使用 | -| clear_page_range(mmu, start,end) | 从线性地址 start 到 end 通过反复释放页表和清除页中间目录项来清除进程页表的内容 | - -#3 线性地址转换 -------- - -##3.1 分页模式下的的线性地址转换 -------- - - -线性地址、页表和页表项线性地址不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图: - -![这里写图片描述](http://img.blog.csdn.net/20160228213803610) - -在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。 - -###3.1.1 页表 -------- - - -多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。 - -###3.1.2 页表项 -------- - - -页表项从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图如下: - -![这里写图片描述](http://img.blog.csdn.net/20160228214056971) - -这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。 - -不管是那一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。上图中的物理页基地址是 个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最 终要访问数据所在物理页的基地址。 - -###3.1.3 地址转换过程 -------- - - 地址转换过程有了上述的基本知识,就很好理解四级页表模式下如何将虚拟地址转化为逻辑地址了。基本过程如下: - -* 1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。 - -* 2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。 - -* 3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。 - -* 4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。 - -* 5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。 - -* 6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。 - -* 7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。 - -* 8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。 - -* 9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。 - -* 10.第五次读取内存得到最终要访问的数据。 - -整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单 位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。 - -![内存索引](http://img.blog.csdn.net/20160806142343800) - -![内存索引](http://img.blog.csdn.net/20160806142316752) - - -##3.2 Linux中通过4级页表访问物理内存 -------- - - -linux中每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。 - -进程的pgd_t数据见 task_struct -> mm_struct -> pgd_t * pgd; - -PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。 - -前面我们讲了页表处理的大多数函数信息,在上面我们又讲了线性地址如何转换为物理地址,其实就是不断索引的过程。 - -通过如下几个函数,不断向下索引,就可以从进程的页表中搜索特定地址对应的页面对象 -| 宏函数| 说明 | -| ------------- |:-------------:| -| pgd_offset | 根据当前虚拟地址和当前进程的mm_struct获取pgd项 | -| pud_offset | 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址 | -| pmd_offset | 根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址) | -| pte_offset | 根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址) | - -根据虚拟地址获取物理页的示例代码详见`mm/memory.c`中的函数`follow_page` ->不同的版本可能有所不同,早起内核中存在follow_page,而后来的内核中被follow_page_mask替代,目前最新的发布4.4中为查找到此函数 - -我们从早期的linux-3.8的源代码中, 截取的代码如下 -``` -/** - * follow_page - look up a page descriptor from a user-virtual address - * @vma: vm_area_struct mapping @address - * @address: virtual address to look up - * @flags: flags modifying lookup behaviour - * - * @flags can have FOLL_ flags set, defined in - * - * Returns the mapped (struct page *), %NULL if no mapping exists, or - * an error pointer if there is a mapping to something not represented - * by a page descriptor (see also vm_normal_page()). - */ -struct page *follow_page(struct vm_area_struct *vma, unsigned long address, - unsigned int flags) -{ - pgd_t *pgd; - pud_t *pud; - pmd_t *pmd; - pte_t *ptep, pte; - spinlock_t *ptl; - struct page *page; - struct mm_struct *mm = vma->vm_mm; - - page = follow_huge_addr(mm, address, flags & FOLL_WRITE); - if (!IS_ERR(page)) { - BUG_ON(flags & FOLL_GET); - goto out; - } - - page = NULL; - pgd = pgd_offset(mm, address); - if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd))) - goto no_page_table; - - pud = pud_offset(pgd, address); - if (pud_none(*pud)) - goto no_page_table; - if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) { - BUG_ON(flags & FOLL_GET); - page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE); - goto out; - } - if (unlikely(pud_bad(*pud))) - goto no_page_table; - - pmd = pmd_offset(pud, address); - if (pmd_none(*pmd)) - goto no_page_table; - if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) { - BUG_ON(flags & FOLL_GET); - page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE); - goto out; - } - if (pmd_trans_huge(*pmd)) { - if (flags & FOLL_SPLIT) { - split_huge_page_pmd(mm, pmd); - goto split_fallthrough; - } - spin_lock(&mm->page_table_lock); - if (likely(pmd_trans_huge(*pmd))) { - if (unlikely(pmd_trans_splitting(*pmd))) { - spin_unlock(&mm->page_table_lock); - wait_split_huge_page(vma->anon_vma, pmd); - } else { - page = follow_trans_huge_pmd(mm, address, - pmd, flags); - spin_unlock(&mm->page_table_lock); - goto out; - } - } else - spin_unlock(&mm->page_table_lock); - /* fall through */ - } -split_fallthrough: - if (unlikely(pmd_bad(*pmd))) - goto no_page_table; - - ptep = pte_offset_map_lock(mm, pmd, address, &ptl); - - pte = *ptep; - if (!pte_present(pte)) - goto no_page; - if ((flags & FOLL_WRITE) && !pte_write(pte)) - goto unlock; - - page = vm_normal_page(vma, address, pte); - if (unlikely(!page)) { - if ((flags & FOLL_DUMP) || - !is_zero_pfn(pte_pfn(pte))) - goto bad_page; - page = pte_page(pte); - } - - if (flags & FOLL_GET) - get_page(page); - if (flags & FOLL_TOUCH) { - if ((flags & FOLL_WRITE) && - !pte_dirty(pte) && !PageDirty(page)) - set_page_dirty(page); - /* - * pte_mkyoung() would be more correct here, but atomic care - * is needed to avoid losing the dirty bit: it is easier to use - * mark_page_accessed(). - */ - mark_page_accessed(page); - } - if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) { - /* - * The preliminary mapping check is mainly to avoid the - * pointless overhead of lock_page on the ZERO_PAGE - * which might bounce very badly if there is contention. - * - * If the page is already locked, we don't need to - * handle it now - vmscan will handle it later if and - * when it attempts to reclaim the page. - */ - if (page->mapping && trylock_page(page)) { - lru_add_drain(); /* push cached pages to LRU */ - /* - * Because we lock page here and migration is - * blocked by the pte's page reference, we need - * only check for file-cache page truncation. - */ - if (page->mapping) - mlock_vma_page(page); - unlock_page(page); - } - } -unlock: - pte_unmap_unlock(ptep, ptl); -out: - return page; - -bad_page: - pte_unmap_unlock(ptep, ptl); - return ERR_PTR(-EFAULT); - -no_page: - pte_unmap_unlock(ptep, ptl); - if (!pte_none(pte)) - return page; - -no_page_table: - /* - * When core dumping an enormous anonymous area that nobody - * has touched so far, we don't want to allocate unnecessary pages or - * page tables. Return error instead of NULL to skip handle_mm_fault, - * then get_dump_page() will return NULL to leave a hole in the dump. - * But we can only make this optimization where a hole would surely - * be zero-filled if handle_mm_fault() actually did handle it. - */ - if ((flags & FOLL_DUMP) && - (!vma->vm_ops || !vma->vm_ops->fault)) - return ERR_PTR(-EFAULT); - return page; -} -``` - -以上代码可以精简为 - -```cpp -unsigned long v2p(int pid unsigned long va) -{ - unsigned long pa = 0; - struct task_struct *pcb_tmp = NULL; - pgd_t *pgd_tmp = NULL; - pud_t *pud_tmp = NULL; - pmd_t *pmd_tmp = NULL; - pte_t *pte_tmp = NULL; - - printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET); - printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT); - printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT); - printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT); - printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT); - - printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD); - printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD); - printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD); - printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE); - - printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK); - - //if(!(pcb_tmp = find_task_by_pid(pid))) - if(!(pcb_tmp = findTaskByPid(pid))) - { - printk(KERN_INFO"Can't find the task %d .\n",pid); - return 0; - } - printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd); - - /* 判断给出的地址va是否合法(va<vm_end)*/ - if(!find_vma(pcb_tmp->mm,va)) - { - printk(KERN_INFO"virt_addr 0x%lx not available.\n",va); - return 0; - } - - pgd_tmp = pgd_offset(pcb_tmp->mm,va); - printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp); - printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp)); - if(pgd_none(*pgd_tmp)) - { - printk(KERN_INFO"Not mapped in pgd.\n"); - return 0; - } - - pud_tmp = pud_offset(pgd_tmp,va); - printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp); - printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp)); - if(pud_none(*pud_tmp)) - { - printk(KERN_INFO"Not mapped in pud.\n"); - return 0; - } - - pmd_tmp = pmd_offset(pud_tmp,va); - printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp); - printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp)); - if(pmd_none(*pmd_tmp)) - { - printk(KERN_INFO"Not mapped in pmd.\n"); - return 0; - } - - /*在这里,把原来的pte_offset_map()改成了pte_offset_kernel*/ - pte_tmp = pte_offset_kernel(pmd_tmp,va); - - printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp); - printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp)); - if(pte_none(*pte_tmp)) - { - printk(KERN_INFO"Not mapped in pte.\n"); - return 0; - } - if(!pte_present(*pte_tmp)){ - printk(KERN_INFO"pte not in RAM.\n"); - return 0; - } - - pa = (pte_val(*pte_tmp) & PAGE_MASK) | (va & ~PAGE_MASK); - printk(KERN_INFO"virt_addr 0x%lx in RAM is 0x%lx t .\n",va,pa); - printk(KERN_INFO"contect in 0x%lx is 0x%lx\n", pa, *(unsigned long *)((char *)pa + PAGE_OFFSET) -} +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-09-01 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/details/52384791) | + +>[注意] +> +>如果您当前使用的系统并不是linux,或者您的系统中只有一份linux源码,而您又期待能够查看或者检索不同版本的linux源码 +> +>可以使用 http://lxr.free-electrons.com/ +> +>LXR (Linux Cross Reference)是比较流行的linux源代码查看工具,而这里集成了全版本的linux源码的索引 + + +#1 linux的分页机制 +------- + + +##1.1 四级分页机制 +------- + + +前面我们提到Linux内核仅使用了较少的分段机制,但是却对分页机制的依赖性很强,其使用一种适合32位和64位结构的通用分页模型,该模型使用**四级分页机制**,即 + +* 页全局目录(Page Global Directory) +* 页上级目录(Page Upper Directory) +* 页中间目录(Page Middle Directory) +* 页表(Page Table) + +>* 页全局目录包含若干页上级目录的地址; +>* 页上级目录又依次包含若干页中间目录的地址; +>* 而页中间目录又包含若干页表的地址; +>* 每一个页表项指向一个页框。 +>因此线性地址因此被分成五个部分,而每一部分的大小与具体的计算机体系结构有关。 + + +![Linux四级页表](http://img.blog.csdn.net/20160806144330104) + + +##1.3 不同架构的分页机制 +------- + + +>对于不同的体系结构,Linux采用的四级页表目录的大小有所不同:对于i386而言,仅采用二级页表,即页上层目录和页中层目录长度为0;对于启用PAE的i386,采用了三级页表,即页上层目录长度为0;对于64位体系结构,可以采用三级或四级页表,具体选择由硬件决定。 + +对于没有启用物理地址扩展的32位系统,两级页表已经足够了。从本质上说Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项而实现的。 + +启用了物理地址扩展的32 位系统使用了三级页表。Linux 的页全局目录对应80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。 + +最终,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。 + + +##1.4 为什么linux热衷:分页>分段 +------- + +那么,为什么Linux是如此地热衷使用分页技术而对分段机制表现得那么地冷淡呢,因为Linux的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行: + +* 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。 +* 区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又被装在不同的页框中。这就是虚拟内存机制的基本要素。 + +每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。 + +把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械式的任务。 + +#2 linux中页表处理数据结构 +------- + +##2.1 页表类型定义pgd_t、pmd_t、pud_t和pte_t +------- +Linux分别采用pgd_t、pmd_t、pud_t和pte_t四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种 数据结构本质上都是无符号长整型unsigned long + +Linux为了更严格数据类型检查,将无符号长整型unsigned long分别封装成四种不同的页表项。如果不采用这种方法,那么一个无符号长整型数据可以传入任何一个与四种页表相关的函数或宏中,这将大大降低程序的健壮性。 + +>pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。 + +首先我们查看一下子这些类型是如何定义的 + +###2.1.1 pteval_t,pmdval_t,pudval_t,pgdval_t +------- + + +>参照[arch/x86/include/asm/pgtable_64_types.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable_64_types.h#L227) + +``` +#ifndef __ASSEMBLY__ +#include + +/* + * These are used to make use of C type-checking.. + */ +typedef unsigned long pteval_t; +typedef unsigned long pmdval_t; +typedef unsigned long pudval_t; +typedef unsigned long pgdval_t; +typedef unsigned long pgprotval_t; + +typedef struct { pteval_t pte; } pte_t; + +#endif /* !__ASSEMBLY__ */ +``` +###2.1.2 pgd_t、pmd_t、pud_t和pte_t +------- + + +>参照 [/arch/x86/include/asm/pgtable_types.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable_types.h#L227) + +``` +typedef struct { pgdval_t pgd; } pgd_t; + +static inline pgd_t native_make_pgd(pgdval_t val) +{ + return (pgd_t) { val }; +} + +static inline pgdval_t native_pgd_val(pgd_t pgd) +{ + return pgd.pgd; +} + +static inline pgdval_t pgd_flags(pgd_t pgd) +{ + return native_pgd_val(pgd) & PTE_FLAGS_MASK; +} + +#if CONFIG_PGTABLE_LEVELS > 3 +typedef struct { pudval_t pud; } pud_t; + +static inline pud_t native_make_pud(pmdval_t val) +{ + return (pud_t) { val }; +} + +static inline pudval_t native_pud_val(pud_t pud) +{ + return pud.pud; +} +#else +#include + +static inline pudval_t native_pud_val(pud_t pud) +{ + return native_pgd_val(pud.pgd); +} +#endif + +#if CONFIG_PGTABLE_LEVELS > 2 +typedef struct { pmdval_t pmd; } pmd_t; + +static inline pmd_t native_make_pmd(pmdval_t val) +{ + return (pmd_t) { val }; +} + +static inline pmdval_t native_pmd_val(pmd_t pmd) +{ + return pmd.pmd; +} +#else +#include + +static inline pmdval_t native_pmd_val(pmd_t pmd) +{ + return native_pgd_val(pmd.pud.pgd); +} +#endif + +static inline pudval_t pud_pfn_mask(pud_t pud) +{ + if (native_pud_val(pud) & _PAGE_PSE) + return PHYSICAL_PUD_PAGE_MASK; + else + return PTE_PFN_MASK; +} + +static inline pudval_t pud_flags_mask(pud_t pud) +{ + return ~pud_pfn_mask(pud); +} + +static inline pudval_t pud_flags(pud_t pud) +{ + return native_pud_val(pud) & pud_flags_mask(pud); +} + +static inline pmdval_t pmd_pfn_mask(pmd_t pmd) +{ + if (native_pmd_val(pmd) & _PAGE_PSE) + return PHYSICAL_PMD_PAGE_MASK; + else + return PTE_PFN_MASK; +} + +static inline pmdval_t pmd_flags_mask(pmd_t pmd) +{ + return ~pmd_pfn_mask(pmd); +} + +static inline pmdval_t pmd_flags(pmd_t pmd) +{ + return native_pmd_val(pmd) & pmd_flags_mask(pmd); +} + +static inline pte_t native_make_pte(pteval_t val) +{ + return (pte_t) { .pte = val }; +} + +static inline pteval_t native_pte_val(pte_t pte) +{ + return pte.pte; +} + +static inline pteval_t pte_flags(pte_t pte) +{ + return native_pte_val(pte) & PTE_FLAGS_MASK; +} +``` + + +###2.1.4 xxx_val和__xxx +------- + + +>参照[/arch/x86/include/asm/pgtable.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable.h#L227) + +五个类型转换宏(__ pte、__ pmd、__ pud、__ pgd和__ pgprot)把一个无符号整数转换成所需的类型。 + +另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。 +``` +#define pgd_val(x) native_pgd_val(x) +#define __pgd(x) native_make_pgd(x) + +#ifndef __PAGETABLE_PUD_FOLDED +#define pud_val(x) native_pud_val(x) +#define __pud(x) native_make_pud(x) +#endif + +#ifndef __PAGETABLE_PMD_FOLDED +#define pmd_val(x) native_pmd_val(x) +#define __pmd(x) native_make_pmd(x) +#endif + +#define pte_val(x) native_pte_val(x) +#define __pte(x) native_make_pte(x) +``` + + +这里需要区别指向页表项的指针和页表项所代表的数据。以pgd_t类型为例子,如果已知一个pgd_t类型的指针pgd,那么通过pgd_val(*pgd)即可获得该页表项(也就是一个无符号长整型数据),这里利用了面向对象的思想。 + +##2.2 页表描述宏 +------- + + +参照[arch/x86/include/asm/pgtable_64](http://lxr.free-electrons.com/source/arch/x86/include/asm/pgtable_64_types.h#L227) +linux中使用下列宏简化了页表处理,对于每一级页表都使用有以下三个关键描述宏: +| 宏字段| 描述 | +| ------------- |:-------------:| -----:| +| XXX_SHIFT| 指定Offset字段的位数 | +| XXX_SIZE| 页的大小 | +| XXX_MASK| 用以屏蔽Offset字段的所有位。 | + +我们的四级页表,对应的宏分别由PAGE,PMD,PUD,PGDIR +| 宏字段前缀| 描述 | +| ------------- |:-------------:| -----:| +| PGDIR | 页全局目录(Page Global Directory) | +| PUD | 页上级目录(Page Upper Directory) | +| PMD | 页中间目录(Page Middle Directory) | +| PAGE | 页表(Page Table) | + +###2.2.1 PAGE宏--页表(Page Table) +------- + +| 字段| 描述 | +| ------------- |:-------------:| -----:| +| PAGE_SHIFT| 指定Offset字段的位数 | +| PAGE_SIZE| 页的大小 | +| PAGE_MASK| 用以屏蔽Offset字段的所有位。 | + +定义如下,在`/arch/x86/include/asm/page_types.h` 文件中 + +``` +/* PAGE_SHIFT determines the page size */ + #define PAGE_SHIFT 12 + #define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT) + #define PAGE_MASK (~(PAGE_SIZE-1)) +``` +当用于80x86处理器时,PAGE_SHIFT返回的值为12。 +由于页内所有地址都必须放在Offset字段, 因此80x86系统的页的大小PAGE_SIZE是$2^{12} =4096$字节。 +PAGE_MASK宏产生的值为0xfffff000,用以屏蔽Offset字段的所有位。 + +###2.2.2 PMD-Page Middle Directory (页目录) +------- + + +| 字段| 描述 | +| ------------- |:-------------:| -----:| +| PMD_SHIFT| 指定线性地址的Offset和Table字段的总位数;换句话说,是页中间目录项可以映射的区域大小的对数 | +| PMD_SIZE| 用于计算由页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小 | +| PMD_MASK| 用于屏蔽Offset字段与Table字段的所有位 | + +**当PAE 被禁用时**,PMD_SHIFT 产生的值为22(来自Offset 的12 位加上来自Table 的10 位), +PMD_SIZE 产生的值为222 或 4 MB, +PMD_MASK产生的值为 0xffc00000。 + +相反,**当PAE被激活时**, +PMD_SHIFT 产生的值为21 (来自Offset的12位加上来自Table的9位), +PMD_SIZE 产生的值为$2^{21}$ 或2 MB +PMD_MASK产生的值为 0xffe00000。 + +>大型页不使用最后一级页表,所以产生大型页尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。 + +###2.2.3 PUD_SHIFT-页上级目录(Page Upper Directory) +------- + + +| 字段| 描述 | +| ------------- |:-------------:| -----:| +| PUD_SHIFT| 确定页上级目录项能映射的区域大小的位数 | +| PUD_SIZE| 用于计算页全局目录中的一个单独表项所能映射的区域大小。 | +| PUD_MASK| 用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位 | + +在80x86处理器上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4MB或2MB。 + +###2.2.4 PGDIR_SHIFT-页全局目录(Page Global Directory) +------- + + +| 字段| 描述 | +| ------------- |:-------------:| -----:| +| PGDIR_SHIFT| 确定页全局页目录项能映射的区域大小的位数 | +| PGDIR_SIZE| 用于计算页全局目录中一个单独表项所能映射区域的大小 | +| PGDIR_MASK| 用于屏蔽Offset, Table,Middle Air及Upper Air的所有位 | + +**当PAE 被禁止时**, +PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同), +PGDIR_SIZE 产生的值为 222 或 4 MB, +PGDIR_MASK 产生的值为 0xffc00000。 + +相反,**当PAE被激活时**, +PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air), +PGDIR_SIZE 产生的值为230 或 1 GB +PGDIR_MASK产生的值为0xc0000000 + +PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD + +用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。 + +##2.3 页表处理函数 +------- + + +>**[注意]** +> 以下内容主要参见 [深入理解linux内核第二章内存寻址中页表处理](http://www.bubuko.com/infodetail-1185213.html) + + +内核还提供了许多宏和函数用于读或修改页表表项: + +* 如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。 + +* 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear( )函数清除一个页表项并返回前一个值。 + +* set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。set_pte_atomic与set_pte作用相同,但是当PAE被激活时它同样能保证64位的值能被原子地写入。 + +* 如果a和b两个页表项指向同一页并且指定相同访问优先级,pte_same(a,b)返回1,否则返回0。 + +* 如果页中间目录项指向一个大型页(2MB或4MB),pmd_large(e)返回1,否则返回0。 + +宏pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这个宏产生的值为1: + +* 页不在主存中(Present标志被清除)。 + +* 页只允许读访问(Read/Write标志被清除)。 + +* Acessed或者Dirty位被清除(对于每个现有的页表,Linux总是 +强制设置这些标志)。 + +pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。 + +如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。 + +前面讲过页表项的Page Size标志对微处理器的分页部件来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其Present和Page Size分别标记为0和1。 + +这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检测到产生异常并不是因为缺页。 + +如果相应表项的Present标志等于1,也就是说,如果对应的页或页表被装载入主存,pmd_present宏产生的值为1。pud_present宏和pgd_present宏产生的值总是1。 + +###2.3.1 查询页表项中任意一个标志的当前值 +------- + + +下表中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其他函数只有在pte_present返回1的时候,才能正常返回页表项中任意一个标志。 +| 函数名称 | 说明 | +| ------------- |:-------------:| -----:| +| pte_user( ) | 读 User/Supervisor 标志 | +| pte_read( ) | 读 User/Supervisor 标志(表示 80x86 处理器上的页不受读的保护)| +| pte_write( ) | 读 Read/Write 标志 | +| pte_exec( ) | 读 User/Supervisor 标志( 80x86 处理器上的页不受代码执行的保护)| +| pte_dirty( ) | 读 Dirty 标志 | +| pte_young( ) | 读 Accessed 标志 | +| pte_file( ) | 读 Dirty 标志(当 Present 标志被清除而 Dirty 标志被设置时,页属于一个非线性磁盘文件映射)| + + +###2.3.2 设置页表项中各标志的值 +------- + + +下表列出的另一组函数用于设置页表项中各标志的值 + +| 函数名称 | 说明 | +| ------------- |:-------------:| +| mk_pte_huge( ) | 设置页表项中的 Page Size 和 Present 标志 | +| pte_wrprotect( ) | 清除 Read/Write 标志 | +| pte_rdprotect( ) | 清除 User/Supervisor 标志 | +| pte_exprotect( ) | 清除 User/Supervisor 标志 | +| pte_mkwrite( ) | 设置 Read/Write 标志 | +| pte_mkread( ) | 设置 User/Supervisor 标志 | +| pte_mkexec( ) | 设置 User/Supervisor 标志 | +| pte_mkclean( ) | 清除 Dirty 标志 | +| pte_mkdirty( ) | 设置 Dirty 标志 | +| pte_mkold( ) | 清除 Accessed 标志(把此页标记为未访问)| +| pte_mkyoung( ) | 设置 Accessed 标志(把此页标记为访问过)| +| pte_modify(p,v) | 把页表项 p 的所有访问权限设置为指定的值 | +| ptep_set_wrprotect() | 与 pte_wrprotect( ) 类似,但作用于指向页表项的指针 | +| ptep_set_access_flags( ) | 如果 Dirty 标志被设置为 1 则将页的访问权设置为指定的值,并调用flush_tlb_page() 函数 | +| ptep_mkdirty() | 与 pte_mkdirty( ) 类似,但作用于指向页表项的指针。| +| ptep_test_and_clear_dirty( ) | 与 pte_mkclean( ) 类似,但作用于指向页表项的指针并返回 Dirty 标志的旧值 | +| ptep_test_and_clear_young( ) | 与 pte_mkold( ) 类似,但作用于指向页表项的指针并返回 Accessed标志的旧值 | + +###2.3.3 宏函数-把一个页地址和一组保护标志组合成页表项,或者执行相反的操作 +------- + + +现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。 +| 宏名称 | 说明 | +| ------------- |:-------------:| +| pgd_index(addr) | 找到线性地址 addr 对应的的目录项在页全局目录中的索引(相对位置)| +| pgd_offset(mm, addr) | 接收内存描述符地址 mm 和线性地址 addr 作为参数。这个宏产生地址addr 在页全局目录中相应表项的线性地址;通过内存描述符 mm 内的一个指针可以找到这个页全局目录 | +| pgd_offset_k(addr) | 产生主内核页全局目录中的某个项的线性地址,该项对应于地址 addr | +| pgd_page(pgd) | 通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址。在两级或三级分页系统中,该宏等价于 pud_page() ,后者应用于页上级目录项 | +| pud_offset(pgd, addr) | 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址 | +| pud_page(pud) | 通过页上级目录项 pud 产生相应的页中间目录的线性地址。在两级分页系统中,该宏等价于 pmd_page() ,后者应用于页中间目录项 | +| pmd_index(addr) | 产生线性地址 addr 在页中间目录中所对应目录项的索引(相对位置)| +| pmd_offset(pud, addr) | 接收指向页上级目录项的指针 pud 和线性地址 addr 作为参数。这个宏产生目录项 addr 在页中间目录中的偏移地址。在两级或三级分页系统中,它产生 pud ,即页全局目录项的地址 | +| pmd_page(pmd) | 通过页中间目录项 pmd 产生相应页表的页描述符地址。在两级或三级分页系统中, pmd 实际上是页全局目录中的一项 | +| mk_pte(p,prot) | 接收页描述符地址 p 和一组访问权限 prot 作为参数,并创建相应的页表项 | +| pte_index(addr) | 产生线性地址 addr 对应的表项在页表中的索引(相对位置)| +| pte_offset_kernel(dir,addr) | 线性地址 addr 在页中间目录 dir 中有一个对应的项,该宏就产生这个对应项,即页表的线性地址。另外,该宏只在主内核页表上使用 | +| pte_offset_map(dir, addr) | 接收指向一个页中间目录项的指针 dir 和线性地址 addr 作为参数,它产生与线性地址 addr 相对应的页表项的线性地址。如果页表被保存在高端存储器中,那么内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它们使用不同的临时内核映射 | +| pte_page( x ) | 返回页表项 x 所引用页的描述符地址 | +| pte_to_pgoff( pte ) | 从一个页表项的 pte 字段内容中提取出文件偏移量,这个偏移量对应着一个非线性文件内存映射所在的页 | +| pgoff_to_pte(offset ) | 为非线性文件内存映射所在的页创建对应页表项的内容| + + + +###2.3.4 简化页表项的创建和撤消 +------- + + +下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。 + +如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。 + +| 函数名称 | 说明 | +| ------------- |:-------------:| +| pgd_alloc( mm ) | 分配一个新的页全局目录。如果 PAE 被激活,它还分配三个对应用户态线性地址的子页中间目录。参数 mm( 内存描述符的地址 )在 80x86 构架上被忽略 | +| pgd_free( pgd) | 释放页全局目录中地址为 pgd 的项。如果 PAE 被激活,它还将释放用户态线性地址对应的三个页中间目录 | +| pud_alloc(mm, pgd, addr) | 在两级或三级分页系统下,这个函数什么也不做:它仅仅返回页全局目录项 pgd 的线性地址 | +| pud_free(x) | 在两级或三级分页系统下,这个宏什么也不做 | +| pmd_alloc(mm, pud, addr) | 定义这个函数以使普通三级分页系统可以为线性地址 addr 分配一个新的页中间目录。如果 PAE 未被激活,这个函数只是返回输入参数 pud 的值,也就是说,返回页全局目录中目录项的地址。如果 PAE 被激活,该函数返回线性地址 addr 对应的页中间目录项的线性地址。参数 mm 被忽略 | +| pmd_free(x) | 该函数什么也不做,因为页中间目录的分配和释放是随同它们的父全局目录一同进行的 | +| pte_alloc_map(mm, pmd, addr) | 接收页中间目录项的地址 pmd 和线性地址 addr 作为参数,并返回与 addr 对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表。如果分配了一个新页表, addr 对应的项就被创建,同时 User/Supervisor 标志被设置为 1 。如果页表被保存在高端内存,则内核建立一个临时内核映射,并用 pte_unmap 对它进行释放 | +| pte_alloc_kernel(mm, pmd, addr) | 如果与地址 addr 相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 addr 相关的页表项的线性地址。该函数仅被主内核页表使用 | +| pte_free(pte) | 释放与页描述符指针 pte 相关的页表 | +| pte_free_kernel(pte) | 等价于 pte_free( ) ,但由主内核页表使用 | +| clear_page_range(mmu, start,end) | 从线性地址 start 到 end 通过反复释放页表和清除页中间目录项来清除进程页表的内容 | + +#3 线性地址转换 +------- + +##3.1 分页模式下的的线性地址转换 +------- + + +线性地址、页表和页表项线性地址不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图: + +![这里写图片描述](http://img.blog.csdn.net/20160228213803610) + +在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。 + +###3.1.1 页表 +------- + + +多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。 + +###3.1.2 页表项 +------- + + +页表项从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图如下: + +![这里写图片描述](http://img.blog.csdn.net/20160228214056971) + +这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。 + +不管是那一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。上图中的物理页基地址是 个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最 终要访问数据所在物理页的基地址。 + +###3.1.3 地址转换过程 +------- + + 地址转换过程有了上述的基本知识,就很好理解四级页表模式下如何将虚拟地址转化为逻辑地址了。基本过程如下: + +* 1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。 + +* 2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。 + +* 3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。 + +* 4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。 + +* 5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。 + +* 6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。 + +* 7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。 + +* 8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。 + +* 9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。 + +* 10.第五次读取内存得到最终要访问的数据。 + +整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单 位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。 + +![内存索引](http://img.blog.csdn.net/20160806142343800) + +![内存索引](http://img.blog.csdn.net/20160806142316752) + + +##3.2 Linux中通过4级页表访问物理内存 +------- + + +linux中每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。 + +进程的pgd_t数据见 task_struct -> mm_struct -> pgd_t * pgd; + +PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。 + +前面我们讲了页表处理的大多数函数信息,在上面我们又讲了线性地址如何转换为物理地址,其实就是不断索引的过程。 + +通过如下几个函数,不断向下索引,就可以从进程的页表中搜索特定地址对应的页面对象 +| 宏函数| 说明 | +| ------------- |:-------------:| +| pgd_offset | 根据当前虚拟地址和当前进程的mm_struct获取pgd项 | +| pud_offset | 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址 | +| pmd_offset | 根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址) | +| pte_offset | 根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址) | + +根据虚拟地址获取物理页的示例代码详见`mm/memory.c`中的函数`follow_page` +>不同的版本可能有所不同,早起内核中存在follow_page,而后来的内核中被follow_page_mask替代,目前最新的发布4.4中为查找到此函数 + +我们从早期的linux-3.8的源代码中, 截取的代码如下 +``` +/** + * follow_page - look up a page descriptor from a user-virtual address + * @vma: vm_area_struct mapping @address + * @address: virtual address to look up + * @flags: flags modifying lookup behaviour + * + * @flags can have FOLL_ flags set, defined in + * + * Returns the mapped (struct page *), %NULL if no mapping exists, or + * an error pointer if there is a mapping to something not represented + * by a page descriptor (see also vm_normal_page()). + */ +struct page *follow_page(struct vm_area_struct *vma, unsigned long address, + unsigned int flags) +{ + pgd_t *pgd; + pud_t *pud; + pmd_t *pmd; + pte_t *ptep, pte; + spinlock_t *ptl; + struct page *page; + struct mm_struct *mm = vma->vm_mm; + + page = follow_huge_addr(mm, address, flags & FOLL_WRITE); + if (!IS_ERR(page)) { + BUG_ON(flags & FOLL_GET); + goto out; + } + + page = NULL; + pgd = pgd_offset(mm, address); + if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd))) + goto no_page_table; + + pud = pud_offset(pgd, address); + if (pud_none(*pud)) + goto no_page_table; + if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) { + BUG_ON(flags & FOLL_GET); + page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE); + goto out; + } + if (unlikely(pud_bad(*pud))) + goto no_page_table; + + pmd = pmd_offset(pud, address); + if (pmd_none(*pmd)) + goto no_page_table; + if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) { + BUG_ON(flags & FOLL_GET); + page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE); + goto out; + } + if (pmd_trans_huge(*pmd)) { + if (flags & FOLL_SPLIT) { + split_huge_page_pmd(mm, pmd); + goto split_fallthrough; + } + spin_lock(&mm->page_table_lock); + if (likely(pmd_trans_huge(*pmd))) { + if (unlikely(pmd_trans_splitting(*pmd))) { + spin_unlock(&mm->page_table_lock); + wait_split_huge_page(vma->anon_vma, pmd); + } else { + page = follow_trans_huge_pmd(mm, address, + pmd, flags); + spin_unlock(&mm->page_table_lock); + goto out; + } + } else + spin_unlock(&mm->page_table_lock); + /* fall through */ + } +split_fallthrough: + if (unlikely(pmd_bad(*pmd))) + goto no_page_table; + + ptep = pte_offset_map_lock(mm, pmd, address, &ptl); + + pte = *ptep; + if (!pte_present(pte)) + goto no_page; + if ((flags & FOLL_WRITE) && !pte_write(pte)) + goto unlock; + + page = vm_normal_page(vma, address, pte); + if (unlikely(!page)) { + if ((flags & FOLL_DUMP) || + !is_zero_pfn(pte_pfn(pte))) + goto bad_page; + page = pte_page(pte); + } + + if (flags & FOLL_GET) + get_page(page); + if (flags & FOLL_TOUCH) { + if ((flags & FOLL_WRITE) && + !pte_dirty(pte) && !PageDirty(page)) + set_page_dirty(page); + /* + * pte_mkyoung() would be more correct here, but atomic care + * is needed to avoid losing the dirty bit: it is easier to use + * mark_page_accessed(). + */ + mark_page_accessed(page); + } + if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) { + /* + * The preliminary mapping check is mainly to avoid the + * pointless overhead of lock_page on the ZERO_PAGE + * which might bounce very badly if there is contention. + * + * If the page is already locked, we don't need to + * handle it now - vmscan will handle it later if and + * when it attempts to reclaim the page. + */ + if (page->mapping && trylock_page(page)) { + lru_add_drain(); /* push cached pages to LRU */ + /* + * Because we lock page here and migration is + * blocked by the pte's page reference, we need + * only check for file-cache page truncation. + */ + if (page->mapping) + mlock_vma_page(page); + unlock_page(page); + } + } +unlock: + pte_unmap_unlock(ptep, ptl); +out: + return page; + +bad_page: + pte_unmap_unlock(ptep, ptl); + return ERR_PTR(-EFAULT); + +no_page: + pte_unmap_unlock(ptep, ptl); + if (!pte_none(pte)) + return page; + +no_page_table: + /* + * When core dumping an enormous anonymous area that nobody + * has touched so far, we don't want to allocate unnecessary pages or + * page tables. Return error instead of NULL to skip handle_mm_fault, + * then get_dump_page() will return NULL to leave a hole in the dump. + * But we can only make this optimization where a hole would surely + * be zero-filled if handle_mm_fault() actually did handle it. + */ + if ((flags & FOLL_DUMP) && + (!vma->vm_ops || !vma->vm_ops->fault)) + return ERR_PTR(-EFAULT); + return page; +} +``` + +以上代码可以精简为 + +```cpp +unsigned long v2p(int pid unsigned long va) +{ + unsigned long pa = 0; + struct task_struct *pcb_tmp = NULL; + pgd_t *pgd_tmp = NULL; + pud_t *pud_tmp = NULL; + pmd_t *pmd_tmp = NULL; + pte_t *pte_tmp = NULL; + + printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET); + printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT); + printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT); + printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT); + printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT); + + printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD); + printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD); + printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD); + printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE); + + printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK); + + //if(!(pcb_tmp = find_task_by_pid(pid))) + if(!(pcb_tmp = findTaskByPid(pid))) + { + printk(KERN_INFO"Can't find the task %d .\n",pid); + return 0; + } + printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd); + + /* 判断给出的地址va是否合法(va<vm_end)*/ + if(!find_vma(pcb_tmp->mm,va)) + { + printk(KERN_INFO"virt_addr 0x%lx not available.\n",va); + return 0; + } + + pgd_tmp = pgd_offset(pcb_tmp->mm,va); + printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp); + printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp)); + if(pgd_none(*pgd_tmp)) + { + printk(KERN_INFO"Not mapped in pgd.\n"); + return 0; + } + + pud_tmp = pud_offset(pgd_tmp,va); + printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp); + printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp)); + if(pud_none(*pud_tmp)) + { + printk(KERN_INFO"Not mapped in pud.\n"); + return 0; + } + + pmd_tmp = pmd_offset(pud_tmp,va); + printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp); + printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp)); + if(pmd_none(*pmd_tmp)) + { + printk(KERN_INFO"Not mapped in pmd.\n"); + return 0; + } + + /*在这里,把原来的pte_offset_map()改成了pte_offset_kernel*/ + pte_tmp = pte_offset_kernel(pmd_tmp,va); + + printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp); + printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp)); + if(pte_none(*pte_tmp)) + { + printk(KERN_INFO"Not mapped in pte.\n"); + return 0; + } + if(!pte_present(*pte_tmp)){ + printk(KERN_INFO"pte not in RAM.\n"); + return 0; + } + + pa = (pte_val(*pte_tmp) & PAGE_MASK) | (va & ~PAGE_MASK); + printk(KERN_INFO"virt_addr 0x%lx in RAM is 0x%lx t .\n",va,pa); + printk(KERN_INFO"contect in 0x%lx is 0x%lx\n", pa, *(unsigned long *)((char *)pa + PAGE_OFFSET) +} ``` \ No newline at end of file diff --git a/study/kernel/02-memory/03-initialize/00-initialize/README.md b/study/kernel/02-memory/03-initialize/02-initialize/README.md similarity index 100% rename from study/kernel/02-memory/03-initialize/00-initialize/README.md rename to study/kernel/02-memory/03-initialize/02-initialize/README.md diff --git a/study/kernel/02-memory/03-initialize/02-memblock/README.md b/study/kernel/02-memory/03-initialize/03-memblock/README.md similarity index 96% rename from study/kernel/02-memory/03-initialize/02-memblock/README.md rename to study/kernel/02-memory/03-initialize/03-memblock/README.md index bd9c826..97a0445 100644 --- a/study/kernel/02-memory/03-initialize/02-memblock/README.md +++ b/study/kernel/02-memory/03-initialize/03-memblock/README.md @@ -1,1338 +1,1348 @@ -的启动过程期间的内存管理--memblock分配器 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. - - - - -#1 前景回顾 -------- - - -##1.1 Linux内存管理的层次结构 -------- - - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - -为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - - -* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - -* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - - -* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. - - - -##1.2 启动过程中的内存初始化 - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - -**系统启动** - -首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) - -其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 - - -```cpp -asmlinkage __visible void __init start_kernel(void) -{ - - setup_arch(&command_line); - mm_init_cpumask(&init_mm); - - setup_per_cpu_areas(); - - - build_all_zonelists(NULL, NULL); - page_alloc_init(); - - - /* - * These use large bootmem allocations and must precede - * mem_init(); - * kmem_cache_init(); - */ - mm_init(); - - kmem_cache_init_late(); - - kmemleak_init(); - setup_per_cpu_pageset(); - - rest_init(); -} -``` - - -| 函数 | 功能 | -|:----:|:----:| -| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | -| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | -| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | -| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | -| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | -| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | -| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | -| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | - - -##1.3 今日内容(非bootmem下的memblock内存管理) -------- - - -在引导内核的过程中, 需要使用内存, 而这个时候内核的内存管理并没有被创建, 因此也就需要一种精简的内存管理系统先接受这个工作, 而在初始化完成后, 再将旧的接口废弃, 转而使用强大的buddy系统来进行内存管理. - - -前面我们讲解了引导内存管理bootmem机制, 它基于最先适配算法, 早期的Linux内核在引导阶段都是通过bootmem来完成初期的内存管理的. 但是后来的版本(笔者分析的是3.19)开始把bootmem弃用了,`__alloc_memory_core_aarly()`取代了bootmem的`__alloc_memory_core()`来完成内存分配, 而后者其实就是调用的memblock来分配内的. - - - -memblock算法是linux内核初始化阶段的一个内存分配器,本质上是取代了原来的bootmem算法. memblock实现比较简单,而它的作用就是在page allocator初始化之前来管理内存,完成分配和释放请求. - - -为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. - -这样在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. -这两种机制对提供的API是一致的,因此对用户是透明的 - -参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) - - -```cpp -ifdef CONFIG_NO_BOOTMEM - obj-y += nobootmem.o -else - obj-y += bootmem.o -endif -``` - - -由于接口是一致的, 那么他们共同使用一份 - -| 头文件 | bootmem接口 | nobootmem接口 | -|:-------:|:----------------:|:-------------------:| -| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | - - -Memblock是在早期引导过程中管理内存的方法之一,此时内核内存分配器还没运行. Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. - - - -http://blog.jobbole.com/88452/ - -http://www.maxwellxxx.com/linuxmemblock - - -https://0xax.gitbooks.io/linux-insides/content/mm/linux-mm-1.html - - -#1 memblock的数据结构 -------- - -##1.1 struct memblock结构 -------- - - -首先来看下memblock结构的定义,文件是[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) - -```cpp -struct memblock { - bool bottom_up; /* is bottom up direction? - 如果true, 则允许由下而上地分配内存*/ - phys_addr_t current_limit; /*指出了内存块的大小限制*/ - /* 接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存*/ - struct memblock_type memory; - struct memblock_type reserved; -#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP - struct memblock_type physmem; -#endif -}; -``` - -该结构体包含五个域。 - -| 字段 | 描述 | -|:---:|:----:| -| bottom_up | 表示分配器分配内存的方式
true:从低地址(内核映像的尾部)向高地址分配
false:也就是top-down,从高地址向地址分配内存. | -| current_limit | 指出了内存块的大小限制, 用于限制通过memblock_alloc的内存申请 | -| memory | 是可用内存的集合 | -| reserved | 已分配内存的集合 | -| physmem | 物理内存的集合(需要配置CONFIG_HAVE_MEMBLOCK_PHYS_MAP参数) | - -接下来的三个域描述了内存块的类型 - -* 预留型 - -* 内存型 - -* 物理内存型(需要配置宏CONFIG_HAVE_MEMBLOCK_PHYS_MAP) - - -##1.2 struct memblock_type -------- - -我们现在又接触到了一个数据结构[memblock_type](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40), 它的定义在[include/linux/memblock.h?v=4.7, line 40](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40) - -```cpp -struct memblock_type -{ - unsigned long cnt; /* number of regions */ - unsigned long max; /* size of the allocated array */ - phys_addr_t total_size; /* size of all regions */ - struct memblock_region *regions; -}; -``` - - -该结构体存储的是内存类型信息 - - - -| 字段 | 描述 | -|:---:|:----:| -| cnt | 当前集合(memory或者reserved)中记录的内存区域个数 | -| max | 当前集合(memory或者reserved)中可记录的内存区域的最大个数 | -| total_size | 集合记录区域信息大小 | -| regions | 内存区域结构指针 | - -它包含的域分别描述了当前内存块含有的内存区域数量, - -所有内存区域的总共大小,已经分配的内存区域大小和一个指向memblock_region结构体的数组指针 - - -##1.3 内存区域memblock_region -------- - -memblock_region结构体描述了内存区域,它的定义在它的定义在[include/linux/memblock.h?v=4.7, line 31](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L31) - - -```cpp -struct memblock_region -{ - phys_addr_t base; - phys_addr_t size; - unsigned long flags; -#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP - int nid; -#endif -}; -``` -| 字段 | 描述 | -|:---:|:----:| -| base | 内存区域起始地址 | -| size | 内存区域大小 | -| flags | 标记 | -| nid | node号 | - - -##1.4 内存区域标识 -------- - - -memblock_region的flags字段存储了当期那内存域的标识信息, 标识用enum变量来定义, 参见[include/linux/memblock.h?v=4.7, line 23](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L23) - -```cpp - -/* Definition of memblock flags. */ -enum { - MEMBLOCK_NONE = 0x0, /* No special request */ - MEMBLOCK_HOTPLUG = 0x1, /* hotpluggable region */ - MEMBLOCK_MIRROR = 0x2, /* mirrored region */ - MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */ -}; -``` - -##1.5 结构总体布局 -------- - -图示法可以用来展示以上结构体之间的关系: - -```cpp -+---------------------------+ +---------------------------+ -| memblock | | | -| _______________________ | | | -| | memory | | | Array of the | -| | memblock_type |-|-->| membock_region | -| |_______________________| | | | -| | +---------------------------+ -| _______________________ | +---------------------------+ -| | reserved | | | | -| | memblock_type |-|-->| Array of the | -| |_______________________| | | memblock_region | -| | | | -+---------------------------+ +---------------------------+ - - - -``` -Memblock主要包含三个结构体:memblock, memblock_type和memblock_region。现在我们已了解了Memblock, 接下来我们将看到Memblock的初始化过程。 - - -##1.6 初始化memblock静态变量 -------- - - -在编译时,会分配好memblock结构所需要的内存空间, 文件是[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L28) - -结构体memblock的初始化变量名和结构体名相同memblock - - -```cpp - -static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; -static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; -#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP -static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock; -#endif - - -struct memblock memblock __initdata_memblock = { - .memory.regions = memblock_memory_init_regions, - .memory.cnt = 1, /* empty dummy entry */ - .memory.max = INIT_MEMBLOCK_REGIONS, - - .reserved.regions = memblock_reserved_init_regions, - .reserved.cnt = 1, /* empty dummy entry */ - .reserved.max = INIT_MEMBLOCK_REGIONS, - -#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP - .physmem.regions = memblock_physmem_init_regions, - .physmem.cnt = 1, /* empty dummy entry */ - .physmem.max = INIT_PHYSMEM_REGIONS, -#endif - - .bottom_up = false, - .current_limit = MEMBLOCK_ALLOC_ANYWHERE, -}; -``` - - - -**__initdata_memblock宏指定存储位置** - -我们可以注意到初始化使用了__initdata_memblock宏,它的定义在[include/linux/memblock.h?v=4.7, line 64](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L64), 如下所示 - -```cpp -#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK -#define __init_memblock __meminit -#define __initdata_memblock __meminitdata -#else -#define __init_memblock -#define __initdata_memblock -#endif -``` - -如果启用`CONFIG_ARCH_DISCARD_MEMBLOCK`宏配置选项,memblock代码会被放到.init代码段, 在内核启动完成后 memblock代码会从.init代码段释放。 - - -**3个memblock_type的初始化** - - -接下来的是memblock结构体中3个memblock_type类型数据 **memory**, **reserved**和**physmem**的初始化 - - - -它们的**memblock_typecnt域**(当前集合中区域个数)被初始化为1. -**memblock_typemax域**(当前集合中最大区域个数)被初始化为`INIT_MEMBLOCK_REGIONS`和`INIT_PHYSMEM_REGIONS` - - - -其中`INIT_MEMBLOCK_REGIONS`为128, 参见[include/linux/memblock.h?v=4.7, line 20](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L20) - -```cpp -#define INIT_MEMBLOCK_REGIONS 128 -#define INIT_PHYSMEM_REGIONS 4 -``` - -而**memblock_type.regions**域都是通过memblock_region数组初始化的, 所有的数组定义都带有__initdata_memblock宏 - - -memblock结构体中最后两个域**bottom_up**内存分配模式被禁用(bottom_up = false, 因此内存分配方式为top-down.), 当前 Memblock的大小限制是`MEMBLOCK_ALLOC_ANYWHERE`为~(phys_addr_t)0即为0xffffffff. - -```cpp -/* Flags for memblock_alloc_base() amd __memblock_alloc_base() */ -#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) -#define MEMBLOCK_ALLOC_ACCESSIBLE 0 -``` - - - - - - -#2 Memblock-API函数接口 -------- - -##2.1 Memblock-API函数接口 -------- - - -既然内核静态创建并初始化了__initdata_memblock这个变量, 那么memblock又是怎么运作的呢? - - -在上文中我提到过所有关于memblock的实现都在[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)源文件中 - - -抛开其他的先不谈,如果要使用memblock,最上层函数一共就4个 - -```cpp -///////////////////////////////////////// -// 基本接口 -///////////////////////////////////////// -// 向memory区中添加内存区域. -memblock_add(phys_addr_t base, phys_addr_t size) - -// 向memory区中删除区域. -memblock_remove(phys_addr_t base, phys_addr_t size) - -// 申请内存 -memblock_alloc(phys_addr_t size, phys_addr_t align) - - -// 释放内存 -memblock_free(phys_addr_t base, phys_addr_t size) - - -///////////////////////////////////////// -// 查找 & 遍历 -///////////////////////////////////////// -// 在给定的范围内找到未使用的内存 -phys_addr_t memblock_find_in_range(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align) - -// 反复迭代 memblock -for_each_mem_range(i, type_a, type_b, nid, flags, p_start, p_end, p_nid) - - - - -///////////////////////////////////////// -// 获取信息 -///////////////////////////////////////// -// 获取内存区域信息 -phys_addr_t get_allocated_memblock_memory_regions_info(phys_addr_t *addr); -// 获取预留内存区域信息 -phys_addr_t get_allocated_memblock_reserved_regions_info(phys_addr_t *addr); - -///////////////////////////////////////// -// 获取信息 -///////////////////////////////////////// -#define memblock_dbg(fmt, ...) \ - if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) - -``` - - -大致翻看了一下内核代码, 发现很少使用memblock_free(),因为很多地方都是申请了内存做永久使用的. 再者,其实在内核中通过memblock_alloc来分配内存其实比较少,一般都是在调用memblock底层的一些函数来简单粗暴的分配的. - - -##2.2 memblock_add将内存区域加入到memblock中 -------- - -###2.2.1 memblock_add函数 -------- - - -memblock_add函数负责向memory区中添加内存区域, 有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblock。 - -memblock_add函数本身并没有什么, 它只是调用了[memblock_add_range函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609)来完成工作, 定义在[mm/memblock.c?v=4.7, line 609](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609) - -```cpp -int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - 0UL, (void *)_RET_IP_); - return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); -``` - - -memblock_add传递的参数依次是 : 内存块类型(memory), 物理基址, 内存区域大小, 最大节点数(0如果CONFIG_NODES_SHIFT没有在配置文件中设置,不然就是CONFIG_NODES_SHIFT)和标志 - -###2.2.2 memblock_add_range函数代码 -------- - -memblock_add_range函数添加新的内存区域到内存块中, 定义在[mm/memblock.c?v=4.7, line 504](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L504) - - - - -* 首先,该函数检查给定的内存区域大小, 如果是0就返回. - -* 在这之后, memblock_add_range用给定的memblock_type检查memblock结构体中是否存在内存区域 - -* 如果没有,我们就用给定的值填充新的memory_region然后返回 - -* 如果memblock_type不为空,我们就把新的内存区域添加到memblock_type类型的memblock中。 - -```cpp -/** - * memblock_add_range - add new memblock region - * @type: memblock type to add new region into - * @base: base address of the new region - * @size: size of the new region - * @nid: nid of the new region - * @flags: flags of the new region - * - * Add new memblock region [@base,@base+@size) into @type. The new region - * is allowed to overlap with existing ones - overlaps don't affect already - * existing regions. @type is guaranteed to be minimal (all neighbouring - * compatible regions are merged) after the addition. - * - * RETURNS: - * 0 on success, -errno on failure. - */ -int __init_memblock memblock_add_range(struct memblock_type *type, - phys_addr_t base, phys_addr_t size, - int nid, unsigned long flags) -{ - bool insert = false; - phys_addr_t obase = base; - /* 获取内存区域的结束位置, - * memblock_cap_size函数会设置size大小确保base + size不会溢出 */ - phys_addr_t end = base + memblock_cap_size(base, &size); - int idx, nr_new; - struct memblock_region *rgn; - - if (!size) - return 0; - - /* special case for empty array */ - if (type->regions[0].size == 0) { - WARN_ON(type->cnt != 1 || type->total_size); - type->regions[0].base = base; - type->regions[0].size = size; - type->regions[0].flags = flags; - memblock_set_region_node(&type->regions[0], nid); - type->total_size = size; - return 0; - } -repeat: - /* - * The following is executed twice. Once with %false @insert and - * then with %true. The first counts the number of regions needed - * to accomodate the new area. The second actually inserts them. - */ - base = obase; - nr_new = 0; - - for_each_memblock_type(type, rgn) { - phys_addr_t rbase = rgn->base; - phys_addr_t rend = rbase + rgn->size; - - if (rbase >= end) - break; - if (rend <= base) - continue; - /* - * @rgn overlaps. If it separates the lower part of new - * area, insert that portion. - */ - if (rbase > base) { -#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP - WARN_ON(nid != memblock_get_region_node(rgn)); -#endif - WARN_ON(flags != rgn->flags); - nr_new++; - if (insert) - memblock_insert_region(type, idx++, base, - rbase - base, nid, - flags); - } - /* area below @rend is dealt with, forget about it */ - base = min(rend, end); - } - - /* insert the remaining portion */ - if (base < end) { - nr_new++; - if (insert) - memblock_insert_region(type, idx, base, end - base, - nid, flags); - } - - /* - * If this was the first round, resize array and repeat for actual - * insertions; otherwise, merge and return. - */ - if (!insert) { - while (type->cnt + nr_new > type->max) - if (memblock_double_array(type, obase, size) < 0) - return -ENOMEM; - insert = true; - goto repeat; - } else { - memblock_merge_regions(type); - return 0; - } -} -``` - -###2.2.3 memblock_add_range函数流程解析 -------- - -首先,我们用如下代码获得内存区域的结束位置: - -```cpp -phys_addr_t end = base + memblock_cap_size(base, &size); -``` - -memblock_cap_size函数会设置size大小确保base + size不会溢出。该函数实现相当简单, 参见[mm/memblock.c?v=4.7, line 79](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L79) - - -```cpp -/* adjust *@size so that (@base + *@size) doesn't overflow, return new size */ -static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size) -{ - return *size = min(*size, (phys_addr_t)ULLONG_MAX - base); -} -``` - - -memblock_cap_size返回size和ULLONG_MAX - base中的最小值 - -在那之后我们得到了新的内存区域的结束地址, 然后 - -* 查内存区域是否重叠 - -* 将新的添加到memblock, 并且看是否能和已经添加到memblock中的内存区域进行合并 - -首先遍历所有已经存储的内存区域并检查有没有和新的内存区域重叠 - - -```cpp -for_each_memblock_type(type, rgn) { - phys_addr_t rbase = rgn->base; - phys_addr_t rend = rbase + rgn->size; - - if (rbase >= end) - break; - if (rend <= base) - continue; - /* ...... */ - /* area below @rend is dealt with, forget about it */ - base = min(rend, end); -} -``` - -如果新内存区域没有和已经存储在memblock的内存区域重叠, 把该新内存区域插入到memblock中. 如果有重叠通通过一个小巧的来完成冲突处理 - -```cpp -base = min(rend, end); -``` - - - -重叠检查完毕后, 新的内存区域已经是一块干净的不包含重叠区域的内存, 把新的内存区域插入到memblock中包含两步: - -* 把新的内存区域中非重叠的部分作为独立的区域加入到memblock - -* 合并所有相邻的内存区域 - -这个过程分为两次循环来完成, 由一个[标识变量insert](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L582)和[report代码跳转标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L543)控制 - -* 第一次循环的时候, 检查新内存区域是否可以放入内存块中并调用memblock_double_array, 而由于[insert = false](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L524), 则执行[!insert条件语句标记的代码块](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L591), 并设置[insert = true](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L595), 然后[goto 跳转到report标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L596)继续开始第二次循环 - -* 第二次循环中, insert = true, 则执行相应的insert == true的代码块, 并且执行[memblock_insert_region将新内存区域插入](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L583), 最后执行[memblock_merge_regions(type)](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L598)合并内存区域 - -这是第一次循环, 我们需要检查新内存区域是否可以放入内存块中并调用memblock_double_array: - -```cpp -/* - * If this was the first round, resize array and repeat for actual - * insertions; otherwise, merge and return. - */ -if (!insert) { /* 第一次执行的的时候insert == false */ - while (type->cnt + nr_new > type->max) - if (memblock_double_array(type, obase, size) < 0) - return -ENOMEM; - insert = true; - goto repeat; -} else { - /* ...... */ -} -``` -memblock_double_array函数加倍给定的内存区域大小,然后把insert设为true再转到repeat标签. - -第二次循环,从repeat标签开始经过同样的循环然后用memblock_insert_region函数把当前内存区域插入到内存块: - -```cpp -/* insert the remaining portion */ -if (base < end) { - nr_new++; - if (insert) - memblock_insert_region(type, idx, base, end - base, - nid, flags); -} -``` - -由于我们在第一次循环中把insert设为true, 现在memblock_insert_region函数将会被调用 - - -[memblock_insert_region函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)几乎和把新内存区域插入到空的memblock_type代码块有同样的实现, 定义在[mm/memblock.c?v=4.7, line 476](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)该函数获得最后一个内存区域: - -```cpp -struct memblock_region *rgn = &type->regions[idx]; -``` - -然后调用memmove函数移动该内存区域: - -```cpp -memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn)); -``` - -紧接着填充新内存区域memblock_region的base域,size域等等, 然后增大memblock_type的大小。 - -最后memblock_add_range函数调用[memblock_merge_regions](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444)合并所有相邻且兼容的内存区域, 定义在[mm/memblock.c?v=4.7, line 444](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444) - - -```cpp -/* - * If this was the first round, resize array and repeat for actual - * insertions; otherwise, merge and return. - */ -if (!insert) { - /* ...... */ -} else { - memblock_merge_regions(type); - return 0; -} -``` - -##2.3 memblock_remove删除内存区域 -------- - - -memblock_remove用来完成删除内存区域的工作, 该函数定义在[mm/memblock.c?v=4.7, line 710](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710) - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710 -int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size) -{ - return memblock_remove_range(&memblock.memory, base, size); -} -``` - - -##2.4 memblock_alloc申请内存 -------- - -而相比来说, 申请内存的函数memblock_alloc实现方式就比较麻烦了, 如下所示 - -`emblock_alloc(phys_addr_t size, phys_addr_t align`)其实就是在当前NODE在内存范围0-MEMBLOCK_ALLOC_ACCESSIBLE(其实是current_limit)中分配一个大小为size的内存区域. - - -###2.4.1 memblock_alloc函数代码 -------- - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 -phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align) -{ - return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE); -} - - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1192 -phys_addr_t __init memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) -{ - phys_addr_t alloc; - - alloc = __memblock_alloc_base(size, align, max_addr); - - if (alloc == 0) - panic("ERROR: Failed to allocate 0x%llx bytes below 0x%llx.\n", - (unsigned long long) size, (unsigned long long) max_addr); - - return alloc; -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1186 -phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) -{ - return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, - MEMBLOCK_NONE); -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1163 -phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) -{ - return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, - MEMBLOCK_NONE); -} -``` -memblock_alloc()很粗暴的从能用的内存里分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数 - -最终memblock_alloc的也是通过memblock_alloc_range_nid函数来完成内存分配的 - - - -###2.4.2 memblock_alloc_range_nid函数 -------- - - -下面我们就来看看memblock_alloc_range_nid函数的实现, 该函数定义在[mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133 -static phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size, - phys_addr_t align, phys_addr_t start, - phys_addr_t end, int nid, ulong flags) -{ - phys_addr_t found; - - if (!align) - align = SMP_CACHE_BYTES; - - found = memblock_find_in_range_node(size, align, start, end, nid, - flags); - if (found && !memblock_reserve(found, size)) { - /* - * The min_count is set to 0 so that memblock allocations are - * never reported as leaks. - */ - kmemleak_alloc(__va(found), size, 0, 0); - return found; - } - return 0; -} -``` - - -memblock_alloc_range_nid函数的主要工作如下 - -* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 - -* memblock_reserve后将其标为已经分配 - - -###2.4.3 memblock_find_in_range_node函数 -------- - -该函数定义在[mm/memblock.c?v=4.7, lien 178](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178) - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178 -phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size, - phys_addr_t align, phys_addr_t start, - phys_addr_t end, int nid, ulong flags) -{ - phys_addr_t kernel_end, ret; - - /* pump up @end */ - if (end == MEMBLOCK_ALLOC_ACCESSIBLE) - end = memblock.current_limit; - - /* avoid allocating the first page */ - start = max_t(phys_addr_t, start, PAGE_SIZE); - end = max(start, end); - kernel_end = __pa_symbol(_end); - - /* - * try bottom-up allocation only when bottom-up mode - * is set and @end is above the kernel image. - */ - if (memblock_bottom_up() && end > kernel_end) { - phys_addr_t bottom_up_start; - - /* make sure we will allocate above the kernel */ - bottom_up_start = max(start, kernel_end); - - /* ok, try bottom-up allocation first */ - ret = __memblock_find_range_bottom_up(bottom_up_start, end, - size, align, nid, flags); - if (ret) - return ret; - - /* - * we always limit bottom-up allocation above the kernel, - * but top-down allocation doesn't have the limit, so - * retrying top-down allocation may succeed when bottom-up - * allocation failed. - * - * bottom-up allocation is expected to be fail very rarely, - * so we use WARN_ONCE() here to see the stack trace if - * fail happens. - */ - WARN_ONCE(1, "memblock: bottom-up allocation failed, memory hotunplug may be affected\n"); - } - - return __memblock_find_range_top_down(start, end, size, align, nid, - flags); -} -``` -* 如果从memblock_alloc过来, end就是MEMBLOCK_ALLOC_ACCESSIBLE,这个时候会设置为current_limit. - -* 如果不通过memblock_alloc分配, 内存范围就是指定的范围. 紧接着对start做调整,为的是避免申请到第一个页面 - -memblock_bottom_up返回的是memblock.bottom_up,前面初始化的时候也知道这个值是false(在numa初始化时会设置为true),所以初始化前期应该调用的是__memblock_find_range_top_down函数去查找内存: - - - - -###2.4.4 __memblock_find_range_top_down查找内存区域 -------- - -最后通过[__memblock_find_range_top_down](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140)函数去查找内存 - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140 -static phys_addr_t __init_memblock -__memblock_find_range_top_down(phys_addr_t start, phys_addr_t end, - phys_addr_t size, phys_addr_t align, int nid, - ulong flags) -{ - phys_addr_t this_start, this_end, cand; - u64 i; - - for_each_free_mem_range_reverse(i, nid, flags, &this_start, &this_end, - NULL) { - this_start = clamp(this_start, start, end); - this_end = clamp(this_end, start, end); - - if (this_end < size) - continue; - - cand = round_down(this_end - size, align); - if (cand >= this_start) - return cand; - } - - return 0; -} -``` - - -* 函数通过使用for_each_free_mem_range_reverse宏封装调用__next_free_mem_range_rev()函数,此函数逐一将memblock.memory里面的内存块信息提取出来与memblock.reserved的各项信息进行检验,确保返回的this_start和this_end不会是分配过的内存块。 - -* 然后通过clamp取中间值,判断大小是否满足,满足的情况下,将自末端向前(因为这是top-down申请方式)的size大小的空间的起始地址(前提该地址不会超出this_start)返回回去 - -至此满足要求的内存块算是找到了。 - - -###2.4.5 memblock_reserve标记申请的内存 -------- - - -现在我们回到[memblock_alloc_range_nid函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133), 我们说该函数完成了两项工作 - -* 首先通过memblock_find_in_range_node指定内存区域和大小查找内存区域 - -* 找到内存区域后, 调用memblock_reserve后将其标为已经分配 - -现在我们已经找到了内存区域了, 那么我们继续看看memblock_reserve函数是如何堆内存进行标记的, 该函数定义在[mm/memblock.c?v=4.7, line 727](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727) - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 -int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg("memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - 0UL, (void *)_RET_IP_); - - return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0); -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609 -int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - 0UL, (void *)_RET_IP_); - - return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); -} -``` - -我们会发现首先memblock_reserve函数也是通过memblock_add_range来实现的, 我们把memblock_add的实现贴出来进行对比, 我们会发现他们就第一个参数不一样 - -* memblock_reserve使用全局变量memblock的reserved域, 最终将分配到的内存块信息添加到reserved区域中 - -* emblock_add则使用了全局变量的memory域, 最终将内存块添加到了memory区域 - - -memblock_add_range函数的流程我们前面已经将的很详细了, 这里只简单的叙述一下子 - -* 如果memblock算法管理内存为空的时候,则将当前空间添加进去 - -* 不为空的情况下,则先检查是否存在内存重叠的情况,如果有的话,则剔除重叠部分,然后将其余非重叠的部分添加进去 - -* 如果出现region[]数组空间不够的情况,则通过memblock_double_array()添加新的region[]空间 - -* 最后通过memblock_merge_regions()把紧挨着的内存合并了 - - - - - -##2.5 memblock_free释放内存区域 -------- - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L695 -static int __init_memblock memblock_remove_range(struct memblock_type *type, - phys_addr_t base, phys_addr_t size) -{ - int start_rgn, end_rgn; - int i, ret; - - ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn); - if (ret) - return ret; - - for (i = end_rgn - 1; i >= start_rgn; i--) - memblock_remove_region(type, i); -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L716 -int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg(" memblock_free: [%#016llx-%#016llx] %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - (void *)_RET_IP_); - - kmemleak_free_part(__va(base), size); - return memblock_remove_range(&memblock.reserved, base, size); -} -``` -#3 兼容bootmem的接口API -------- - - - -##3.1 memblock与bootmem -------- - -我们了解到memblock是作为bootmem的替代品而引入内核的 - -在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. -这两种机制对提供的API是一致的,因此对用户是透明的 - -参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) - - -```cpp -ifdef CONFIG_NO_BOOTMEM - obj-y += nobootmem.o -else - obj-y += bootmem.o -endif -``` - - -为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. - - - -##3.2 memblock与bootmem接口对比 -------- - - -由于接口是一致的, 那么他们共同使用一份 - -| 头文件 | bootmem接口 | nobootmem接口 | -|:-------:|:----------------:|:-------------------:| -| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | - - - -
-我们知道memblock自己的接口都在自己的头文件和源文件中 - -| memblock 头文件 | memblock接口 | -|:--------------:|:------------:| -| [include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) | [mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7) - - -为了实现接口兼容, 内核用[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)定义的memblock接口, 实现了一套bootmem的接口机制, 而bootmem的接口我们在上一篇引导内存管理bootmem机制中已经讲过了, 这些实现的bootmem函数接口API, 就定义在[mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c)文件中, 然后内核把他们进行了封装, 然后提供了与bootmem相同功能和函数的接口, 这些接口都在[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7). - - - -在NUMA系统上, 基本的API是相同的, 但是函数增加了_node后缀, 与UMA系统的函数相比, 还需要一些额外的参数, 用于指定内存分配的结点. - - -| 函数 | bootmem定义 | nobootmem定义 | -|:-----:|:------------------:|:----------------------:| -| ZONE_NORMAL的分配函数 | 按照指定大小在ZONE_NORMAL内存域分配函数. 数据是对齐的, 这使得内存或者从可适用于L1高速缓存的理想位置开始| | -| alloc_bootmem(size) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L700)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?=4.7#L672) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | -| alloc_bootmem_align(size) | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
基于__alloc_bootmem实现 | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | -| alloc_bootmem_pages(size)) | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | -| alloc_bootmem_nopanic(size) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L664)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L261)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | -| ZONE_DMA区域的分配函数 | | -| alloc_bootmem_low(size) | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
[__alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L415)底层基于___alloc_bootmem | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
底层基于___alloc_bootmem | -| alloc_bootmem_low_pages_nopanic(size) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | -| alloc_bootmem_low_pages(size) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421) | -| NUMA结构的分配函数 | | | -| alloc_bootmem_node(pgdat, size) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L777)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L383)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_node_nopanic(pgdat, size) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L738)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_pages_node(pgdat, size) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_pages_node_nopanic(pgdat, size) | [alloc_bootmem_pages_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L138)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_low_pages_node(pgdat, size) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L861)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L707) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L444)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L353)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | - - -我们可以看到最基本的实现思路都是一样的, 只是最底层的实现函数有细微的区别. - - -##3.3 实现差异 -------- - - -* UMA结构下这些函数最终都是通过___alloc_bootmem_nopanic函数来实现的 - -* NUMA架构下, 最终这些函数都是简介的调用___alloc_bootmem_node_nopanic函数来实现的, - - - - -| 函数 | bootmem | memblock | -|:-----:|:-----------:|:------------:| -| ___alloc_bootmem_nopanic | [mm/bootmem.c?v=4.7, line 632](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632), 通过[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)函数来实现 | [mm/nobootmem.c?v=4.7, line 235](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235), 通过[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | -| ___alloc_bootmem_node_nopanic | [mm/bootmem.c?v=4.7, line 708](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708), 通过调用[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)和[alloc_bootmem_bdata](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L500)来实现 | [mm/nobootmem.c?v=4.7, line 317](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317), 通过调用[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36)来实现 | - - -bootmem的核心函数__alloc_memory_core()的实现机制我们前一篇博文[引导分配器bootmem](待添加链接)已经讲过了, 那么memblock下nobootmem的核心函数__alloc_memory_core_early是怎么实现的呢? - -前面[2.4.1节memblock_alloc函数代码](待添加链接)我们分析memblock_alloc函数的时候提到, 该函数最终通过memblock_alloc_range_nid函数粗暴粗暴的进行内存分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数, 这里的__alloc_memory_core_early函数就是基于memblock_alloc_range_nid同样的思路实现的函数 - - -* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 - -* memblock_reserve后将其标为已经分配 - - -我们列出 memblock_alloc_range_nid函数与__alloc_memory_core_early函数的实现对比 - -| memblock_alloc_range_nid | __alloc_memory_core_early | -|:------------------------------:|:------------------------------:| -| [mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) | [mm/nobootmem.c?v=4.7, line 36](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | - - -#4 memblock初始化 -------- - -如果从整个linux生命周期来讲,涉及到各种初始化等,这里来详细分析,因为还没有分析完内核,所以这里是分析到哪里就记录到哪里了. - -##4.1 x86架构下的memblock初始化 -------- - -要理解memblock是如何工作和实现的, 我们首先看一下它的用法. - - - -在Linux内核中有几处用到了memblock, 例如 arch/x86/kernel/e820.c中的函数`memblock_x86_fill`. 该函数遍历由`e820`提供的内存映射表并且通过`memblock_add`函数把内核预留的内存区域添加到memblock。既然我们首先遇到了`memblock_add`函数,那就从它开始吧。 - - -在内核初始化初期,物理内存会通过`Int 0x15`来被探测和整理, 存放到`e820`中.而初始化就发生在这个以后. 参见[arch/x86/kernel/setup.c?v=4.7, line 1096](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.7#L1096) - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* - * Need to conclude brk, before memblock_x86_fill() - * it could use memblock_find_in_range, could overlap with - * brk area. - */ - reserve_brk(); - - cleanup_highmap(); - - memblock_set_current_limit(ISA_END_ADDRESS); - memblock_x86_fill(); -} -``` - -首先内核建立内核页表需要扩展__brk, 而扩展后的brk就立即被声明为已分配. 这项工作是由reserve_brk通过调用memblock_reserve完成的, 而其实并不是正真通过memblock分配的, 因为此时memblock还没有完成初始化 - -[reserve_brk](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209)函数定义在[arch/x86/kernel/setup.c?v=4.7, line 209](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209), 此时memblock还没有初始化, 只能通过[memblock_reserve](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727)来完成内存的分配 - -```cpp -static void __init reserve_brk(void) -{ - if (_brk_end > _brk_start) - memblock_reserve(__pa_symbol(_brk_start), - _brk_end - _brk_start); - - /* Mark brk area as locked down and no longer taking any - new allocations */ - _brk_start = 0; -} -``` - -设置完__brk后, 可以看到,setup_arch()函数通过memblock_x86_fill(),依据e820中的信息来初始化memblock. - -```cpp -void __init memblock_x86_fill(void) -{ - int i; - u64 end; - - /* - * EFI may have more than 128 entries - * We are safe to enable resizing, beause memblock_x86_fill() - * is rather later for x86 - */ - memblock_allow_resize(); - - for (i = 0; i < e820.nr_map; i++) { - struct e820entry *ei = &e820.map[i]; - - end = ei->addr + ei->size; - if (end != (resource_size_t)end) - continue; - - if (ei->type != E820_RAM && ei->type != E820_RESERVED_KERN) - continue; - - memblock_add(ei->addr, ei->size); - } - - /* throw away partial pages */ - memblock_trim_memory(PAGE_SIZE); - - memblock_dump_all(); -} -``` - -比较简单,通过e820中的信息memblock_add(),将内存添加到memblock中的memory中,当做可分配内存.后两个函数主要是修剪内存使之对齐和输出信息. - -至此, 我们的memblock就初始化好了, 简单而且粗暴 - - - -##4.2 arm架构下的memblock初始化 -------- - - -arm下的memblock初始化也是从start_kernel()->[setup_arch()](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c#L1034)开始的, 在setup_arch()中arm架构通过arm_memblock_init完成了memblock的初始化工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - arm_memblock_init(mdesc); -} -``` - -arm_memblock_init定义在[arch/arm/mm/init.c](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L230), 如下所示 - - -```cpp -void __init arm_memblock_init(const struct machine_desc *mdesc) -{ - /* Register the kernel text, kernel data and initrd with memblock. */ -#ifdef CONFIG_XIP_KERNEL - memblock_reserve(__pa(_sdata), _end - _sdata); -#else - memblock_reserve(__pa(_stext), _end - _stext); -#endif -#ifdef CONFIG_BLK_DEV_INITRD - /* FDT scan will populate initrd_start */ - if (initrd_start && !phys_initrd_size) { - phys_initrd_start = __virt_to_phys(initrd_start); - phys_initrd_size = initrd_end - initrd_start; - } - initrd_start = initrd_end = 0; - if (phys_initrd_size && - !memblock_is_region_memory(phys_initrd_start, phys_initrd_size)) { - pr_err("INITRD: 0x%08llx+0x%08lx is not a memory region - disabling initrd\n", - (u64)phys_initrd_start, phys_initrd_size); - phys_initrd_start = phys_initrd_size = 0; - } - if (phys_initrd_size && - memblock_is_region_reserved(phys_initrd_start, phys_initrd_size)) { - pr_err("INITRD: 0x%08llx+0x%08lx overlaps in-use memory region - disabling initrd\n", - (u64)phys_initrd_start, phys_initrd_size); - phys_initrd_start = phys_initrd_size = 0; - } - if (phys_initrd_size) { - memblock_reserve(phys_initrd_start, phys_initrd_size); - - /* Now convert initrd to virtual addresses */ - initrd_start = __phys_to_virt(phys_initrd_start); - initrd_end = initrd_start + phys_initrd_size; - } -#endif - - arm_mm_memblock_reserve(); - - /* reserve any platform specific memblock areas */ - if (mdesc->reserve) - mdesc->reserve(); - - early_init_fdt_reserve_self(); - early_init_fdt_scan_reserved_mem(); - - /* reserve memory for DMA contiguous allocations */ - dma_contiguous_reserve(arm_dma_limit); - - arm_memblock_steal_permitted = false; - memblock_dump_all(); -} -``` - -##4.3 arm64下的memblock初始化 -------- - -前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* 初始化memblock */ - arm64_memblock_init( ); - - /* 分页机制初始化 */ - paging_init(); - - bootmem_init(); -} -``` - -| 流程 | 描述 | -|:---:|:----:| -| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | -| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | -| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | - - - - -其中arm64_memblock_init就完成了arm64架构下的memblock的初始化 - - -与arm架构类似, arm64的memblock初始化没有意外, 只是初始化函数成为[arm64_memblock_init()](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L261), 该函数定义在[arch/arm64/mm/init.c?v=4.7, line 192](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L192) - - - -#5 总结 -------- - - -memblock内存管理是将所有的物理内存放到`memblock.memory`中作为可用内存来管理, 分配过的内存只加入到`memblock.reserved`中, 并不从`memory`中移出. - -同理释放内存也会加入到`memory`中. 也就是说, `memory`在`fill`过后基本就是不动的了. 申请和分配内存仅仅修改`reserved`就达到目的. 在初始化阶段没有那么多复杂的内存操作场景, 甚至很多地方都是申请了内存做永久使用的, 所以这样的内存管理方式已经足够凑合着用了, 毕竟内核也不指望用它一辈子. 在系统完成初始化之后所有的工作会移交给强大的`buddy`系统来进行内存管理 - +的启动过程期间的内存管理--memblock分配器 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + +在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. + + + + +#1 前景回顾 +------- + + +##1.1 Linux内存管理的层次结构 +------- + + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + + +* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + + +##1.2 启动过程中的内存初始化 + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + +**系统启动** + +首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) + +其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 + + +```cpp +asmlinkage __visible void __init start_kernel(void) +{ + + setup_arch(&command_line); + mm_init_cpumask(&init_mm); + + setup_per_cpu_areas(); + + + build_all_zonelists(NULL, NULL); + page_alloc_init(); + + + /* + * These use large bootmem allocations and must precede + * mem_init(); + * kmem_cache_init(); + */ + mm_init(); + + kmem_cache_init_late(); + + kmemleak_init(); + setup_per_cpu_pageset(); + + rest_init(); +} +``` + + +| 函数 | 功能 | +|:----:|:----:| +| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | +| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | +| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | +| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | +| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | +| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | +| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | +| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | + + +##1.3 今日内容(非bootmem下的memblock内存管理) +------- + + +在引导内核的过程中, 需要使用内存, 而这个时候内核的内存管理并没有被创建, 因此也就需要一种精简的内存管理系统先接受这个工作, 而在初始化完成后, 再将旧的接口废弃, 转而使用强大的buddy系统来进行内存管理. + + +前面我们讲解了引导内存管理bootmem机制, 它基于最先适配算法, 早期的Linux内核在引导阶段都是通过bootmem来完成初期的内存管理的. 但是后来的版本(笔者分析的是3.19)开始把bootmem弃用了,`__alloc_memory_core_aarly()`取代了bootmem的`__alloc_memory_core()`来完成内存分配, 而后者其实就是调用的memblock来分配内的. + + + +memblock算法是linux内核初始化阶段的一个内存分配器,本质上是取代了原来的bootmem算法. memblock实现比较简单,而它的作用就是在page allocator初始化之前来管理内存,完成分配和释放请求. + + +为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. + +这样在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. +这两种机制对提供的API是一致的,因此对用户是透明的 + +参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) + + +```cpp +ifdef CONFIG_NO_BOOTMEM + obj-y += nobootmem.o +else + obj-y += bootmem.o +endif +``` + + +由于接口是一致的, 那么他们共同使用一份 + +| 头文件 | bootmem接口 | nobootmem接口 | +|:-------:|:----------------:|:-------------------:| +| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | + + +Memblock是在早期引导过程中管理内存的方法之一,此时内核内存分配器还没运行. Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. + + + +>参考 +> +>[Linux 内核内存管理(1)](http://blog.jobbole.com/88452) +> +>[Linux内核初期内存管理---memblock](http://www.maxwellxxx.com/linuxmemblock) +> +>[Linux kernel memory management Part 1.](https://0xax.gitbooks.io/linux-insides/content/mm/linux-mm-1.html) + +#2 memblock的数据结构 +------- + +##2.1 struct memblock结构 +------- + + +首先来看下memblock结构的定义,文件是[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) + +```cpp +struct memblock { + bool bottom_up; /* is bottom up direction? + 如果true, 则允许由下而上地分配内存*/ + phys_addr_t current_limit; /*指出了内存块的大小限制*/ + /* 接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存*/ + struct memblock_type memory; + struct memblock_type reserved; +#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP + struct memblock_type physmem; +#endif +}; +``` + +该结构体包含五个域。 + +| 字段 | 描述 | +|:---:|:----:| +| bottom_up | 表示分配器分配内存的方式
true:从低地址(内核映像的尾部)向高地址分配
false:也就是top-down,从高地址向地址分配内存. | +| current_limit | 指出了内存块的大小限制, 用于限制通过memblock_alloc的内存申请 | +| memory | 是可用内存的集合 | +| reserved | 已分配内存的集合 | +| physmem | 物理内存的集合(需要配置CONFIG_HAVE_MEMBLOCK_PHYS_MAP参数) | + +接下来的三个域描述了内存块的类型 + +* 预留型 + +* 内存型 + +* 物理内存型(需要配置宏CONFIG_HAVE_MEMBLOCK_PHYS_MAP) + + +##2.2 struct memblock_type +------- + +我们现在又接触到了一个数据结构[memblock_type](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40), 它的定义在[include/linux/memblock.h?v=4.7, line 40](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40) + +```cpp +struct memblock_type +{ + unsigned long cnt; /* number of regions */ + unsigned long max; /* size of the allocated array */ + phys_addr_t total_size; /* size of all regions */ + struct memblock_region *regions; +}; +``` + + +该结构体存储的是内存类型信息 + + + +| 字段 | 描述 | +|:---:|:----:| +| cnt | 当前集合(memory或者reserved)中记录的内存区域个数 | +| max | 当前集合(memory或者reserved)中可记录的内存区域的最大个数 | +| total_size | 集合记录区域信息大小 | +| regions | 内存区域结构指针 | + +它包含的域分别描述了当前内存块含有的内存区域数量, + +所有内存区域的总共大小,已经分配的内存区域大小和一个指向memblock_region结构体的数组指针 + + +##2.3 内存区域memblock_region +------- + + +memblock_region结构体描述了内存区域,它的定义在它的定义在[include/linux/memblock.h?v=4.7, line 31](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L31) + + +```cpp +struct memblock_region +{ + phys_addr_t base; + phys_addr_t size; + unsigned long flags; +#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP + int nid; +#endif +}; +``` +| 字段 | 描述 | +|:---:|:----:| +| base | 内存区域起始地址 | +| size | 内存区域大小 | +| flags | 标记 | +| nid | node号 | + + +##2.4 内存区域标识 +------- + + +memblock_region的flags字段存储了当期那内存域的标识信息, 标识用enum变量来定义, 参见[include/linux/memblock.h?v=4.7, line 23](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L23) + +```cpp + +/* Definition of memblock flags. */ +enum { + MEMBLOCK_NONE = 0x0, /* No special request */ + MEMBLOCK_HOTPLUG = 0x1, /* hotpluggable region */ + MEMBLOCK_MIRROR = 0x2, /* mirrored region */ + MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */ +}; +``` + +##2.5 结构总体布局 +------- + + +图示法可以用来展示以上结构体之间的关系: + +```cpp ++---------------------------+ +---------------------------+ +| memblock | | | +| _______________________ | | | +| | memory | | | Array of the | +| | memblock_type |-|-->| membock_region | +| |_______________________| | | | +| | +---------------------------+ +| _______________________ | +---------------------------+ +| | reserved | | | | +| | memblock_type |-|-->| Array of the | +| |_______________________| | | memblock_region | +| | | | ++---------------------------+ +---------------------------+ + + + +``` +Memblock主要包含三个结构体:memblock, memblock_type和memblock_region。现在我们已了解了Memblock, 接下来我们将看到Memblock的初始化过程。 + + +##2.6 初始化memblock静态变量 +------- + + +在编译时,会分配好memblock结构所需要的内存空间, 文件是[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L28) + +结构体memblock的初始化变量名和结构体名相同memblock + + +```cpp + +static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; +static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; +#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP +static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock; +#endif + + +struct memblock memblock __initdata_memblock = { + .memory.regions = memblock_memory_init_regions, + .memory.cnt = 1, /* empty dummy entry */ + .memory.max = INIT_MEMBLOCK_REGIONS, + + .reserved.regions = memblock_reserved_init_regions, + .reserved.cnt = 1, /* empty dummy entry */ + .reserved.max = INIT_MEMBLOCK_REGIONS, + +#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP + .physmem.regions = memblock_physmem_init_regions, + .physmem.cnt = 1, /* empty dummy entry */ + .physmem.max = INIT_PHYSMEM_REGIONS, +#endif + + .bottom_up = false, + .current_limit = MEMBLOCK_ALLOC_ANYWHERE, +}; +``` + + + +**__initdata_memblock宏指定存储位置** + +我们可以注意到初始化使用了__initdata_memblock宏,它的定义在[include/linux/memblock.h?v=4.7, line 64](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L64), 如下所示 + +```cpp +#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK +#define __init_memblock __meminit +#define __initdata_memblock __meminitdata +#else +#define __init_memblock +#define __initdata_memblock +#endif +``` + +如果启用`CONFIG_ARCH_DISCARD_MEMBLOCK`宏配置选项,memblock代码会被放到.init代码段, 在内核启动完成后 memblock代码会从.init代码段释放。 + + +**3个memblock_type的初始化** + + +接下来的是memblock结构体中3个memblock_type类型数据 **memory**, **reserved**和**physmem**的初始化 + + + +它们的**memblock_typecnt域**(当前集合中区域个数)被初始化为1. +**memblock_typemax域**(当前集合中最大区域个数)被初始化为`INIT_MEMBLOCK_REGIONS`和`INIT_PHYSMEM_REGIONS` + + + +其中`INIT_MEMBLOCK_REGIONS`为128, 参见[include/linux/memblock.h?v=4.7, line 20](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L20) + +```cpp +#define INIT_MEMBLOCK_REGIONS 128 +#define INIT_PHYSMEM_REGIONS 4 +``` + +而**memblock_type.regions**域都是通过memblock_region数组初始化的, 所有的数组定义都带有__initdata_memblock宏 + + +memblock结构体中最后两个域**bottom_up**内存分配模式被禁用(bottom_up = false, 因此内存分配方式为top-down.), 当前 Memblock的大小限制是`MEMBLOCK_ALLOC_ANYWHERE`为~(phys_addr_t)0即为0xffffffff. + +```cpp +/* Flags for memblock_alloc_base() amd __memblock_alloc_base() */ +#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) +#define MEMBLOCK_ALLOC_ACCESSIBLE 0 +``` + + + + + + +#3 Memblock-API函数接口 +------- + + +##3.1 Memblock-API函数接口 +------- + + +既然内核静态创建并初始化了__initdata_memblock这个变量, 那么memblock又是怎么运作的呢? + + +在上文中我提到过所有关于memblock的实现都在[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)源文件中 + + +抛开其他的先不谈,如果要使用memblock,最上层函数一共就4个 + +```cpp +///////////////////////////////////////// +// 基本接口 +///////////////////////////////////////// +// 向memory区中添加内存区域. +memblock_add(phys_addr_t base, phys_addr_t size) + +// 向memory区中删除区域. +memblock_remove(phys_addr_t base, phys_addr_t size) + +// 申请内存 +memblock_alloc(phys_addr_t size, phys_addr_t align) + + +// 释放内存 +memblock_free(phys_addr_t base, phys_addr_t size) + + +///////////////////////////////////////// +// 查找 & 遍历 +///////////////////////////////////////// +// 在给定的范围内找到未使用的内存 +phys_addr_t memblock_find_in_range(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align) + +// 反复迭代 memblock +for_each_mem_range(i, type_a, type_b, nid, flags, p_start, p_end, p_nid) + + + + +///////////////////////////////////////// +// 获取信息 +///////////////////////////////////////// +// 获取内存区域信息 +phys_addr_t get_allocated_memblock_memory_regions_info(phys_addr_t *addr); +// 获取预留内存区域信息 +phys_addr_t get_allocated_memblock_reserved_regions_info(phys_addr_t *addr); + +///////////////////////////////////////// +// 获取信息 +///////////////////////////////////////// +#define memblock_dbg(fmt, ...) \ + if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) + +``` + + +大致翻看了一下内核代码, 发现很少使用memblock_free(),因为很多地方都是申请了内存做永久使用的. 再者,其实在内核中通过memblock_alloc来分配内存其实比较少,一般都是在调用memblock底层的一些函数来简单粗暴的分配的. + + +##3.2 memblock_add将内存区域加入到memblock中 +------- + +###3.2.1 memblock_add函数 +------- + + +memblock_add函数负责向memory区中添加内存区域, 有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblock。 + +memblock_add函数本身并没有什么, 它只是调用了[memblock_add_range函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609)来完成工作, 定义在[mm/memblock.c?v=4.7, line 609](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609) + +```cpp +int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + 0UL, (void *)_RET_IP_); + return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); +``` + + +memblock_add传递的参数依次是 : 内存块类型(memory), 物理基址, 内存区域大小, 最大节点数(0如果CONFIG_NODES_SHIFT没有在配置文件中设置,不然就是CONFIG_NODES_SHIFT)和标志 + + + +###3.2.2 memblock_add_range函数代码 +------- + + + +memblock_add_range函数添加新的内存区域到内存块中, 定义在[mm/memblock.c?v=4.7, line 504](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L504) + + + + +* 首先,该函数检查给定的内存区域大小, 如果是0就返回. + +* 在这之后, memblock_add_range用给定的memblock_type检查memblock结构体中是否存在内存区域 + +* 如果没有,我们就用给定的值填充新的memory_region然后返回 + +* 如果memblock_type不为空,我们就把新的内存区域添加到memblock_type类型的memblock中。 + +```cpp +/** + * memblock_add_range - add new memblock region + * @type: memblock type to add new region into + * @base: base address of the new region + * @size: size of the new region + * @nid: nid of the new region + * @flags: flags of the new region + * + * Add new memblock region [@base,@base+@size) into @type. The new region + * is allowed to overlap with existing ones - overlaps don't affect already + * existing regions. @type is guaranteed to be minimal (all neighbouring + * compatible regions are merged) after the addition. + * + * RETURNS: + * 0 on success, -errno on failure. + */ +int __init_memblock memblock_add_range(struct memblock_type *type, + phys_addr_t base, phys_addr_t size, + int nid, unsigned long flags) +{ + bool insert = false; + phys_addr_t obase = base; + /* 获取内存区域的结束位置, + * memblock_cap_size函数会设置size大小确保base + size不会溢出 */ + phys_addr_t end = base + memblock_cap_size(base, &size); + int idx, nr_new; + struct memblock_region *rgn; + + if (!size) + return 0; + + /* special case for empty array */ + if (type->regions[0].size == 0) { + WARN_ON(type->cnt != 1 || type->total_size); + type->regions[0].base = base; + type->regions[0].size = size; + type->regions[0].flags = flags; + memblock_set_region_node(&type->regions[0], nid); + type->total_size = size; + return 0; + } +repeat: + /* + * The following is executed twice. Once with %false @insert and + * then with %true. The first counts the number of regions needed + * to accomodate the new area. The second actually inserts them. + */ + base = obase; + nr_new = 0; + + for_each_memblock_type(type, rgn) { + phys_addr_t rbase = rgn->base; + phys_addr_t rend = rbase + rgn->size; + + if (rbase >= end) + break; + if (rend <= base) + continue; + /* + * @rgn overlaps. If it separates the lower part of new + * area, insert that portion. + */ + if (rbase > base) { +#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP + WARN_ON(nid != memblock_get_region_node(rgn)); +#endif + WARN_ON(flags != rgn->flags); + nr_new++; + if (insert) + memblock_insert_region(type, idx++, base, + rbase - base, nid, + flags); + } + /* area below @rend is dealt with, forget about it */ + base = min(rend, end); + } + + /* insert the remaining portion */ + if (base < end) { + nr_new++; + if (insert) + memblock_insert_region(type, idx, base, end - base, + nid, flags); + } + + /* + * If this was the first round, resize array and repeat for actual + * insertions; otherwise, merge and return. + */ + if (!insert) { + while (type->cnt + nr_new > type->max) + if (memblock_double_array(type, obase, size) < 0) + return -ENOMEM; + insert = true; + goto repeat; + } else { + memblock_merge_regions(type); + return 0; + } +} +``` + +###3.2.3 memblock_add_range函数流程解析 +------- + + +首先,我们用如下代码获得内存区域的结束位置: + +```cpp +phys_addr_t end = base + memblock_cap_size(base, &size); +``` + +memblock_cap_size函数会设置size大小确保base + size不会溢出。该函数实现相当简单, 参见[mm/memblock.c?v=4.7, line 79](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L79) + + +```cpp +/* adjust *@size so that (@base + *@size) doesn't overflow, return new size */ +static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size) +{ + return *size = min(*size, (phys_addr_t)ULLONG_MAX - base); +} +``` + + +memblock_cap_size返回size和ULLONG_MAX - base中的最小值 + +在那之后我们得到了新的内存区域的结束地址, 然后 + +* 查内存区域是否重叠 + +* 将新的添加到memblock, 并且看是否能和已经添加到memblock中的内存区域进行合并 + +首先遍历所有已经存储的内存区域并检查有没有和新的内存区域重叠 + + +```cpp +for_each_memblock_type(type, rgn) { + phys_addr_t rbase = rgn->base; + phys_addr_t rend = rbase + rgn->size; + + if (rbase >= end) + break; + if (rend <= base) + continue; + /* ...... */ + /* area below @rend is dealt with, forget about it */ + base = min(rend, end); +} +``` + +如果新内存区域没有和已经存储在memblock的内存区域重叠, 把该新内存区域插入到memblock中. 如果有重叠通通过一个小巧的来完成冲突处理 + +```cpp +base = min(rend, end); +``` + + + +重叠检查完毕后, 新的内存区域已经是一块干净的不包含重叠区域的内存, 把新的内存区域插入到memblock中包含两步: + +* 把新的内存区域中非重叠的部分作为独立的区域加入到memblock + +* 合并所有相邻的内存区域 + +这个过程分为两次循环来完成, 由一个[标识变量insert](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L582)和[report代码跳转标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L543)控制 + +* 第一次循环的时候, 检查新内存区域是否可以放入内存块中并调用memblock_double_array, 而由于[insert = false](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L524), 则执行[!insert条件语句标记的代码块](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L591), 并设置[insert = true](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L595), 然后[goto 跳转到report标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L596)继续开始第二次循环 + +* 第二次循环中, insert = true, 则执行相应的insert == true的代码块, 并且执行[memblock_insert_region将新内存区域插入](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L583), 最后执行[memblock_merge_regions(type)](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L598)合并内存区域 + +这是第一次循环, 我们需要检查新内存区域是否可以放入内存块中并调用memblock_double_array: + +```cpp +/* + * If this was the first round, resize array and repeat for actual + * insertions; otherwise, merge and return. + */ +if (!insert) { /* 第一次执行的的时候insert == false */ + while (type->cnt + nr_new > type->max) + if (memblock_double_array(type, obase, size) < 0) + return -ENOMEM; + insert = true; + goto repeat; +} else { + /* ...... */ +} +``` +memblock_double_array函数加倍给定的内存区域大小,然后把insert设为true再转到repeat标签. + +第二次循环,从repeat标签开始经过同样的循环然后用memblock_insert_region函数把当前内存区域插入到内存块: + +```cpp +/* insert the remaining portion */ +if (base < end) { + nr_new++; + if (insert) + memblock_insert_region(type, idx, base, end - base, + nid, flags); +} +``` + +由于我们在第一次循环中把insert设为true, 现在memblock_insert_region函数将会被调用 + + +[memblock_insert_region函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)几乎和把新内存区域插入到空的memblock_type代码块有同样的实现, 定义在[mm/memblock.c?v=4.7, line 476](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)该函数获得最后一个内存区域: + +```cpp +struct memblock_region *rgn = &type->regions[idx]; +``` + +然后调用memmove函数移动该内存区域: + +```cpp +memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn)); +``` + +紧接着填充新内存区域memblock_region的base域,size域等等, 然后增大memblock_type的大小。 + +最后memblock_add_range函数调用[memblock_merge_regions](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444)合并所有相邻且兼容的内存区域, 定义在[mm/memblock.c?v=4.7, line 444](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444) + + +```cpp +/* + * If this was the first round, resize array and repeat for actual + * insertions; otherwise, merge and return. + */ +if (!insert) { + /* ...... */ +} else { + memblock_merge_regions(type); + return 0; +} +``` + +##3.3 memblock_remove删除内存区域 +------- + + + +memblock_remove用来完成删除内存区域的工作, 该函数定义在[mm/memblock.c?v=4.7, line 710](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710) + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710 +int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size) +{ + return memblock_remove_range(&memblock.memory, base, size); +} +``` + + +##3.4 memblock_alloc申请内存 +------- + + +而相比来说, 申请内存的函数memblock_alloc实现方式就比较麻烦了, 如下所示 + +`emblock_alloc(phys_addr_t size, phys_addr_t align`)其实就是在当前NODE在内存范围0-MEMBLOCK_ALLOC_ACCESSIBLE(其实是current_limit)中分配一个大小为size的内存区域. + + +###3.4.1 memblock_alloc函数代码 +------- + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 +phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align) +{ + return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE); +} + + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1192 +phys_addr_t __init memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) +{ + phys_addr_t alloc; + + alloc = __memblock_alloc_base(size, align, max_addr); + + if (alloc == 0) + panic("ERROR: Failed to allocate 0x%llx bytes below 0x%llx.\n", + (unsigned long long) size, (unsigned long long) max_addr); + + return alloc; +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1186 +phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) +{ + return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, + MEMBLOCK_NONE); +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1163 +phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) +{ + return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, + MEMBLOCK_NONE); +} +``` +memblock_alloc()很粗暴的从能用的内存里分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数 + +最终memblock_alloc的也是通过memblock_alloc_range_nid函数来完成内存分配的 + + + +###3.4.2 memblock_alloc_range_nid函数 +------- + + +下面我们就来看看memblock_alloc_range_nid函数的实现, 该函数定义在[mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133 +static phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size, + phys_addr_t align, phys_addr_t start, + phys_addr_t end, int nid, ulong flags) +{ + phys_addr_t found; + + if (!align) + align = SMP_CACHE_BYTES; + + found = memblock_find_in_range_node(size, align, start, end, nid, + flags); + if (found && !memblock_reserve(found, size)) { + /* + * The min_count is set to 0 so that memblock allocations are + * never reported as leaks. + */ + kmemleak_alloc(__va(found), size, 0, 0); + return found; + } + return 0; +} +``` + + +memblock_alloc_range_nid函数的主要工作如下 + +* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 + +* memblock_reserve后将其标为已经分配 + + +###3.4.3 memblock_find_in_range_node函数 +------- + +该函数定义在[mm/memblock.c?v=4.7, lien 178](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178) + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178 +phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size, + phys_addr_t align, phys_addr_t start, + phys_addr_t end, int nid, ulong flags) +{ + phys_addr_t kernel_end, ret; + + /* pump up @end */ + if (end == MEMBLOCK_ALLOC_ACCESSIBLE) + end = memblock.current_limit; + + /* avoid allocating the first page */ + start = max_t(phys_addr_t, start, PAGE_SIZE); + end = max(start, end); + kernel_end = __pa_symbol(_end); + + /* + * try bottom-up allocation only when bottom-up mode + * is set and @end is above the kernel image. + */ + if (memblock_bottom_up() && end > kernel_end) { + phys_addr_t bottom_up_start; + + /* make sure we will allocate above the kernel */ + bottom_up_start = max(start, kernel_end); + + /* ok, try bottom-up allocation first */ + ret = __memblock_find_range_bottom_up(bottom_up_start, end, + size, align, nid, flags); + if (ret) + return ret; + + /* + * we always limit bottom-up allocation above the kernel, + * but top-down allocation doesn't have the limit, so + * retrying top-down allocation may succeed when bottom-up + * allocation failed. + * + * bottom-up allocation is expected to be fail very rarely, + * so we use WARN_ONCE() here to see the stack trace if + * fail happens. + */ + WARN_ONCE(1, "memblock: bottom-up allocation failed, memory hotunplug may be affected\n"); + } + + return __memblock_find_range_top_down(start, end, size, align, nid, + flags); +} +``` +* 如果从memblock_alloc过来, end就是MEMBLOCK_ALLOC_ACCESSIBLE,这个时候会设置为current_limit. + +* 如果不通过memblock_alloc分配, 内存范围就是指定的范围. 紧接着对start做调整,为的是避免申请到第一个页面 + +memblock_bottom_up返回的是memblock.bottom_up,前面初始化的时候也知道这个值是false(在numa初始化时会设置为true),所以初始化前期应该调用的是__memblock_find_range_top_down函数去查找内存: + + + + +###3.4.4 __memblock_find_range_top_down查找内存区域 +------- + +最后通过[__memblock_find_range_top_down](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140)函数去查找内存 + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140 +static phys_addr_t __init_memblock +__memblock_find_range_top_down(phys_addr_t start, phys_addr_t end, + phys_addr_t size, phys_addr_t align, int nid, + ulong flags) +{ + phys_addr_t this_start, this_end, cand; + u64 i; + + for_each_free_mem_range_reverse(i, nid, flags, &this_start, &this_end, + NULL) { + this_start = clamp(this_start, start, end); + this_end = clamp(this_end, start, end); + + if (this_end < size) + continue; + + cand = round_down(this_end - size, align); + if (cand >= this_start) + return cand; + } + + return 0; +} +``` + + +* 函数通过使用for_each_free_mem_range_reverse宏封装调用__next_free_mem_range_rev()函数,此函数逐一将memblock.memory里面的内存块信息提取出来与memblock.reserved的各项信息进行检验,确保返回的this_start和this_end不会是分配过的内存块。 + +* 然后通过clamp取中间值,判断大小是否满足,满足的情况下,将自末端向前(因为这是top-down申请方式)的size大小的空间的起始地址(前提该地址不会超出this_start)返回回去 + +至此满足要求的内存块算是找到了。 + + +###3.4.5 memblock_reserve标记申请的内存 +------- + + +现在我们回到[memblock_alloc_range_nid函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133), 我们说该函数完成了两项工作 + +* 首先通过memblock_find_in_range_node指定内存区域和大小查找内存区域 + +* 找到内存区域后, 调用memblock_reserve后将其标为已经分配 + +现在我们已经找到了内存区域了, 那么我们继续看看memblock_reserve函数是如何堆内存进行标记的, 该函数定义在[mm/memblock.c?v=4.7, line 727](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727) + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 +int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg("memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + 0UL, (void *)_RET_IP_); + + return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0); +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609 +int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + 0UL, (void *)_RET_IP_); + + return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); +} +``` + +我们会发现首先memblock_reserve函数也是通过memblock_add_range来实现的, 我们把memblock_add的实现贴出来进行对比, 我们会发现他们就第一个参数不一样 + +* memblock_reserve使用全局变量memblock的reserved域, 最终将分配到的内存块信息添加到reserved区域中 + +* emblock_add则使用了全局变量的memory域, 最终将内存块添加到了memory区域 + + +memblock_add_range函数的流程我们前面已经将的很详细了, 这里只简单的叙述一下子 + +* 如果memblock算法管理内存为空的时候,则将当前空间添加进去 + +* 不为空的情况下,则先检查是否存在内存重叠的情况,如果有的话,则剔除重叠部分,然后将其余非重叠的部分添加进去 + +* 如果出现region[]数组空间不够的情况,则通过memblock_double_array()添加新的region[]空间 + +* 最后通过memblock_merge_regions()把紧挨着的内存合并了 + + + + + +##2.5 memblock_free释放内存区域 +------- + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L695 +static int __init_memblock memblock_remove_range(struct memblock_type *type, + phys_addr_t base, phys_addr_t size) +{ + int start_rgn, end_rgn; + int i, ret; + + ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn); + if (ret) + return ret; + + for (i = end_rgn - 1; i >= start_rgn; i--) + memblock_remove_region(type, i); +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L716 +int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg(" memblock_free: [%#016llx-%#016llx] %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + (void *)_RET_IP_); + + kmemleak_free_part(__va(base), size); + return memblock_remove_range(&memblock.reserved, base, size); +} +``` +#3 兼容bootmem的接口API +------- + + + +##3.1 memblock与bootmem +------- + +我们了解到memblock是作为bootmem的替代品而引入内核的 + +在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. +这两种机制对提供的API是一致的,因此对用户是透明的 + +参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) + + +```cpp +ifdef CONFIG_NO_BOOTMEM + obj-y += nobootmem.o +else + obj-y += bootmem.o +endif +``` + + +为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. + + + +##3.2 memblock与bootmem接口对比 +------- + + +由于接口是一致的, 那么他们共同使用一份 + +| 头文件 | bootmem接口 | nobootmem接口 | +|:-------:|:----------------:|:-------------------:| +| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | + + + +
+我们知道memblock自己的接口都在自己的头文件和源文件中 + +| memblock 头文件 | memblock接口 | +|:--------------:|:------------:| +| [include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) | [mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7) + + +为了实现接口兼容, 内核用[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)定义的memblock接口, 实现了一套bootmem的接口机制, 而bootmem的接口我们在上一篇引导内存管理bootmem机制中已经讲过了, 这些实现的bootmem函数接口API, 就定义在[mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c)文件中, 然后内核把他们进行了封装, 然后提供了与bootmem相同功能和函数的接口, 这些接口都在[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7). + + + +在NUMA系统上, 基本的API是相同的, 但是函数增加了_node后缀, 与UMA系统的函数相比, 还需要一些额外的参数, 用于指定内存分配的结点. + + +| 函数 | bootmem定义 | nobootmem定义 | +|:-----:|:------------------:|:----------------------:| +| ZONE_NORMAL的分配函数 | 按照指定大小在ZONE_NORMAL内存域分配函数. 数据是对齐的, 这使得内存或者从可适用于L1高速缓存的理想位置开始| | +| alloc_bootmem(size) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L700)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?=4.7#L672) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | +| alloc_bootmem_align(size) | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
基于__alloc_bootmem实现 | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | +| alloc_bootmem_pages(size)) | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | +| alloc_bootmem_nopanic(size) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L664)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L261)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | +| ZONE_DMA区域的分配函数 | | +| alloc_bootmem_low(size) | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
[__alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L415)底层基于___alloc_bootmem | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
底层基于___alloc_bootmem | +| alloc_bootmem_low_pages_nopanic(size) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | +| alloc_bootmem_low_pages(size) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421) | +| NUMA结构的分配函数 | | | +| alloc_bootmem_node(pgdat, size) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L777)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L383)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_node_nopanic(pgdat, size) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L738)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_pages_node(pgdat, size) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_pages_node_nopanic(pgdat, size) | [alloc_bootmem_pages_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L138)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_low_pages_node(pgdat, size) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L861)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L707) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L444)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L353)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | + + +我们可以看到最基本的实现思路都是一样的, 只是最底层的实现函数有细微的区别. + + +##3.3 实现差异 +------- + + +* UMA结构下这些函数最终都是通过___alloc_bootmem_nopanic函数来实现的 + +* NUMA架构下, 最终这些函数都是简介的调用___alloc_bootmem_node_nopanic函数来实现的, + + + + +| 函数 | bootmem | memblock | +|:-----:|:-----------:|:------------:| +| ___alloc_bootmem_nopanic | [mm/bootmem.c?v=4.7, line 632](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632), 通过[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)函数来实现 | [mm/nobootmem.c?v=4.7, line 235](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235), 通过[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | +| ___alloc_bootmem_node_nopanic | [mm/bootmem.c?v=4.7, line 708](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708), 通过调用[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)和[alloc_bootmem_bdata](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L500)来实现 | [mm/nobootmem.c?v=4.7, line 317](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317), 通过调用[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36)来实现 | + + +bootmem的核心函数__alloc_memory_core()的实现机制我们前一篇博文[引导分配器bootmem](待添加链接)已经讲过了, 那么memblock下nobootmem的核心函数__alloc_memory_core_early是怎么实现的呢? + +前面[2.4.1节memblock_alloc函数代码](待添加链接)我们分析memblock_alloc函数的时候提到, 该函数最终通过memblock_alloc_range_nid函数粗暴粗暴的进行内存分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数, 这里的__alloc_memory_core_early函数就是基于memblock_alloc_range_nid同样的思路实现的函数 + + +* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 + +* memblock_reserve后将其标为已经分配 + + +我们列出 memblock_alloc_range_nid函数与__alloc_memory_core_early函数的实现对比 + +| memblock_alloc_range_nid | __alloc_memory_core_early | +|:------------------------------:|:------------------------------:| +| [mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) | [mm/nobootmem.c?v=4.7, line 36](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | + + +#4 memblock初始化 +------- + +如果从整个linux生命周期来讲,涉及到各种初始化等,这里来详细分析,因为还没有分析完内核,所以这里是分析到哪里就记录到哪里了. + +##4.1 x86架构下的memblock初始化 +------- + +要理解memblock是如何工作和实现的, 我们首先看一下它的用法. + + + +在Linux内核中有几处用到了memblock, 例如 arch/x86/kernel/e820.c中的函数`memblock_x86_fill`. 该函数遍历由`e820`提供的内存映射表并且通过`memblock_add`函数把内核预留的内存区域添加到memblock。既然我们首先遇到了`memblock_add`函数,那就从它开始吧。 + + +在内核初始化初期,物理内存会通过`Int 0x15`来被探测和整理, 存放到`e820`中.而初始化就发生在这个以后. 参见[arch/x86/kernel/setup.c?v=4.7, line 1096](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.7#L1096) + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* + * Need to conclude brk, before memblock_x86_fill() + * it could use memblock_find_in_range, could overlap with + * brk area. + */ + reserve_brk(); + + cleanup_highmap(); + + memblock_set_current_limit(ISA_END_ADDRESS); + memblock_x86_fill(); +} +``` + +首先内核建立内核页表需要扩展__brk, 而扩展后的brk就立即被声明为已分配. 这项工作是由reserve_brk通过调用memblock_reserve完成的, 而其实并不是正真通过memblock分配的, 因为此时memblock还没有完成初始化 + +[reserve_brk](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209)函数定义在[arch/x86/kernel/setup.c?v=4.7, line 209](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209), 此时memblock还没有初始化, 只能通过[memblock_reserve](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727)来完成内存的分配 + +```cpp +static void __init reserve_brk(void) +{ + if (_brk_end > _brk_start) + memblock_reserve(__pa_symbol(_brk_start), + _brk_end - _brk_start); + + /* Mark brk area as locked down and no longer taking any + new allocations */ + _brk_start = 0; +} +``` + +设置完__brk后, 可以看到,setup_arch()函数通过memblock_x86_fill(),依据e820中的信息来初始化memblock. + +```cpp +void __init memblock_x86_fill(void) +{ + int i; + u64 end; + + /* + * EFI may have more than 128 entries + * We are safe to enable resizing, beause memblock_x86_fill() + * is rather later for x86 + */ + memblock_allow_resize(); + + for (i = 0; i < e820.nr_map; i++) { + struct e820entry *ei = &e820.map[i]; + + end = ei->addr + ei->size; + if (end != (resource_size_t)end) + continue; + + if (ei->type != E820_RAM && ei->type != E820_RESERVED_KERN) + continue; + + memblock_add(ei->addr, ei->size); + } + + /* throw away partial pages */ + memblock_trim_memory(PAGE_SIZE); + + memblock_dump_all(); +} +``` + +比较简单,通过e820中的信息memblock_add(),将内存添加到memblock中的memory中,当做可分配内存.后两个函数主要是修剪内存使之对齐和输出信息. + +至此, 我们的memblock就初始化好了, 简单而且粗暴 + + + +##4.2 arm架构下的memblock初始化 +------- + + +arm下的memblock初始化也是从start_kernel()->[setup_arch()](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c#L1034)开始的, 在setup_arch()中arm架构通过arm_memblock_init完成了memblock的初始化工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + arm_memblock_init(mdesc); +} +``` + +arm_memblock_init定义在[arch/arm/mm/init.c](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L230), 如下所示 + + +```cpp +void __init arm_memblock_init(const struct machine_desc *mdesc) +{ + /* Register the kernel text, kernel data and initrd with memblock. */ +#ifdef CONFIG_XIP_KERNEL + memblock_reserve(__pa(_sdata), _end - _sdata); +#else + memblock_reserve(__pa(_stext), _end - _stext); +#endif +#ifdef CONFIG_BLK_DEV_INITRD + /* FDT scan will populate initrd_start */ + if (initrd_start && !phys_initrd_size) { + phys_initrd_start = __virt_to_phys(initrd_start); + phys_initrd_size = initrd_end - initrd_start; + } + initrd_start = initrd_end = 0; + if (phys_initrd_size && + !memblock_is_region_memory(phys_initrd_start, phys_initrd_size)) { + pr_err("INITRD: 0x%08llx+0x%08lx is not a memory region - disabling initrd\n", + (u64)phys_initrd_start, phys_initrd_size); + phys_initrd_start = phys_initrd_size = 0; + } + if (phys_initrd_size && + memblock_is_region_reserved(phys_initrd_start, phys_initrd_size)) { + pr_err("INITRD: 0x%08llx+0x%08lx overlaps in-use memory region - disabling initrd\n", + (u64)phys_initrd_start, phys_initrd_size); + phys_initrd_start = phys_initrd_size = 0; + } + if (phys_initrd_size) { + memblock_reserve(phys_initrd_start, phys_initrd_size); + + /* Now convert initrd to virtual addresses */ + initrd_start = __phys_to_virt(phys_initrd_start); + initrd_end = initrd_start + phys_initrd_size; + } +#endif + + arm_mm_memblock_reserve(); + + /* reserve any platform specific memblock areas */ + if (mdesc->reserve) + mdesc->reserve(); + + early_init_fdt_reserve_self(); + early_init_fdt_scan_reserved_mem(); + + /* reserve memory for DMA contiguous allocations */ + dma_contiguous_reserve(arm_dma_limit); + + arm_memblock_steal_permitted = false; + memblock_dump_all(); +} +``` + +##4.3 arm64下的memblock初始化 +------- + +前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* 初始化memblock */ + arm64_memblock_init( ); + + /* 分页机制初始化 */ + paging_init(); + + bootmem_init(); +} +``` + +| 流程 | 描述 | +|:---:|:----:| +| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | +| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | +| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | + + + + +其中arm64_memblock_init就完成了arm64架构下的memblock的初始化 + + +与arm架构类似, arm64的memblock初始化没有意外, 只是初始化函数成为[arm64_memblock_init()](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L261), 该函数定义在[arch/arm64/mm/init.c?v=4.7, line 192](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L192) + + + +#5 总结 +------- + + +memblock内存管理是将所有的物理内存放到`memblock.memory`中作为可用内存来管理, 分配过的内存只加入到`memblock.reserved`中, 并不从`memory`中移出. + +同理释放内存也会加入到`memory`中. 也就是说, `memory`在`fill`过后基本就是不动的了. 申请和分配内存仅仅修改`reserved`就达到目的. 在初始化阶段没有那么多复杂的内存操作场景, 甚至很多地方都是申请了内存做永久使用的, 所以这样的内存管理方式已经足够凑合着用了, 毕竟内核也不指望用它一辈子. 在系统完成初始化之后所有的工作会移交给强大的`buddy`系统来进行内存管理 + diff --git a/study/kernel/02-memory/03-initialize/03-paging_init/README.md b/study/kernel/02-memory/03-initialize/06-paging_init/README.md similarity index 100% rename from study/kernel/02-memory/03-initialize/03-paging_init/README.md rename to study/kernel/02-memory/03-initialize/06-paging_init/README.md