我对linux内核四级分页理解

likeyiyy 2014-07-10 12:55:11
我们很容易在网上找到这样一段话,这段话其实是来自于《深入理解linux内核》
Linux分页机制:
作为一个通用的操作系统,Linux需要兼容各种硬件体系,包括不同位数的CPU。对64位的CPU来说,两级页表仍然太少,一个页表会太大,这会占用太多宝贵的物理内存。Linux采用了通用的四级页表。实际采用几级页表则具体受硬件的限制。

四种页表分别称为: 页全局目录、页上级目录、页中间目录、页表。对于32位x86系统,两级页表已经足够了。Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目 录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。

/******************************************************************************************/
很明显这段话是很难理解的,最难理解的地方是:“Linux通过使“页上级目录”位和“页中间目录”位全为0”,我曾经对这段话产生两种理解:

我的第一个理解,大概是和这个帖子类似的:http://www.linuxdiyf.com/viewarticle.php?id=183360

pud和pmd全为0,那该是什么样子啊?这样的话,一个虚拟地址岂不是有一部分必须为0,那么linux内核岂不是不能寻址到4G空间,为了解释我内心的矛盾,我善意的将pud和pmd理解为只有1位,并且这一位为0,于是就是这个样子。


这样的话,pud和pmd都占有两位,并且pud和pmd都会占一页,并且只用了这一页的第一项,巨大的浪费啊,我那时如此感叹。


我产生的第二种理解是,pud和pmd所指向的页表的内容全部为0,把上面的那张图略微p一下,来解释我是怎么理解的。



我以为那个页全部为0 ,那么所有的pmd其实寻找的都是物理地址为0的页了,而物理地址为0的页的每一项都是0。
但是等等,所有的虚拟地址都会经过物理地址0来寻找页,怎么可能,我很快知道了我这种想法的错误。

后来我诞生了第三种想法,和第一种想法类似,pud和pmd不在虚拟地址中占位,但是它们的值默认为0,pgd找到一页后,会取这一页(pud)的第一项继续找,找到一页(pmd)再用这一页的第一项找pte,虽然这种情况下,还是多占用了两个页,但是至少不占地址空间了,大概如下面这位仁兄这么想。
http://bbs.chinaunix.net/thread-1919185-1-1.html

/*************************************************************************************************************************************/
再也不能忍受这种情况了,我决定去内核源代码里去寻找。
相关的头文件为:include/asm-generic/ pgtable.h pgtable-nopmd.h pgtable-nopud.h
源代码在: arch/x86/mm/pgtable.c
当然必要时肯定要参照其他架构下的代码。

首先假设给我们一个虚拟地址,我们自己的想法是什么呢?
1. 根据address的golbale_dir+a3,得到pud_offset
2. pud_offset + upper_dir 得到pmd_offset
3. pmd_offset + middle_dir 得到pte_offset
4. pte_offset+ table 得到页表
5.page_table+offset得到这个地址的这个字节。

内核源代码也是这个流程,除去错误处理,代码如下:
pud = pud_offset(pgd, address);
pmd = pmd_offset(pud, address);
pte = pte_offset_kernel(pmd, address);
下面的没有了,我猜测是因为要做大页小页,或者其他情况的区分,不过我们可以自己猜测,继续下去是这个样子。
//pg = pg_offset(pte,address);
//pa = paddr(pg,address);
我们不管这两个我臆造的函数,看看前面三个函数都做了些什么。

在有全部四级分页的情况下,pud_offset pmd_offset pte_offset_kernel的代码如下:
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}
可以说,这三个函数没有任何出乎我们意料,或者难以理解的部分,就是按照我们的想法走的。
然后在nopud和nopmd的时候,是什么样子的呢?
static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
return (pud_t *)pgd;
}
static inline pmd_t * pmd_offset(pud_t * pud, unsigned long address)
{
return (pmd_t *)pud;
}
这样一来,两级分页的时候,虚拟地址还是被分成了三个部分,pgd,pg,offset,根本没有pgd和pmd好吗!!!!
pud = pud_offset(pgd, address);
pmd = pmd_offset(pud, address);
pte = pte_offset_kernel(pmd, address);
上面这样的流程,直接变成了
pte = pte_offset_kernel(pgd,address);了。
那么现在在理解作者的那句话,全部置为0 ,就是说根本没有了。


有意思的是我在pgtable_nommu.h里看到pmd_offset的宏定义如下:
#define pmd_offset(a, b) ((void *)0)
那么我猜想,其实这是让虚拟地址直接对应物理地址了,说是转换了,其实没转换。

/---------------------------------------------------------------------------------------------------------------------------------/
其实,这么分析下来,我最大的疑问是:内核里为什么会有这样的代码?这些代码什么情况下被调用。
我的意思是说,难道一个虚拟地址到物理地址的转换难道不是硬件完成的吗?难到cpu遇到一个访问内存的指令,就说啊哈,要访问内存了,
怎么把虚拟地址变成物理地址呢?我们执行内核里上面的代码吧。
这可能吗?上面的代码被编译后也是指令,难道为了执行一条指令每次都要执行这些指令才能得到物理地址?(注意这些并不是缺页异常的处理程序。)

我知道肯定不是这样的,内核之所以会拥有这些代码,是因为或许有某种需求需要内核显式的管理内存,或者需要感知整个内存的使用情况时才需要调用这些函数,
说白了是为了内核的管理功能(尽管我不知道是什么),而不是一个程序的执行过程中的虚拟到物理的转换要执行这些代码。

我的理解对吗?

/*****************************************************************************************************************************************/


...全文
1364 5 打赏 收藏 转发到动态 举报
写回复
用AI写文章
5 条回复
切换为时间正序
请发表友善的回复…
发表回复
xyz347 2015-07-05
  • 打赏
  • 举报
回复
说一下个人理解哈,不一定正确。 1、硬件查表不会去“执行”代码,32位系统中就是cr3找pgd,然后找pte 2、“统一成四级映射”只是说内核去构造相应的分页表的时候而已。大概看了一下代码,好像是这样的: a, nopud的情况下(nopmd类似)分配pud是返回空的 b, nopud的情况下获取pud实际返回的是pgd c, 综合上述情况,实际就是构造分页表的时候,首先分配pgd,然后分配pud(NULL),pgd指向null,然后分配pmd(NULL),pud(根据b返回的是pgd)指向pmd,然后pmd(返回的还是pgd)指向pte
likeyiyy 2014-07-11
  • 打赏
  • 举报
回复
没有人评论一下吗?
likeyiyy 2014-07-11
  • 打赏
  • 举报
回复
没人评论,结贴吧,我认为我的探索是正确的。
colddown 2014-07-11
  • 打赏
  • 举报
回复
遇到问题的时候多看内核代码是解决疑问的唯一方法。书只能作为参考,而且有时已经过时了。比如slab内存分配,其实在新内核里基本都用的slub,不但更加简单而且也更实用。
likeyiyy 2014-07-10
  • 打赏
  • 举报
回复
/* 假如一个老人对一个新人说,你看linux就是这样经过地址转换的,不信你看,代码就这样写着呢,这种情况下,会不会导致新人误以为,每个访问内存的地址,都要经过【这些代码】转化呢?这一下子不是误解了吗? 这种误解甚至比不理解页表转换还可怕,对吧。 希望有人告诉我,这些代码是用来干什么的。 */ 在理解linux高端内存的时候,我还是迷糊了,我不明白怎么内核地址减去某个固定值就是物理地址了。 当时我自己的想法如下: /---------------------------------------------------------------------------------------------------------------/ in_my_opinion, 无论内核空间,还是用户空间,它们访问内存的时候都是虚拟地址吧。 当保护位打开的时候,任何地址都会经过mmu的转换吧。 硬件难道因为知道你是内核就不对你的地址进行转换了?(有可能) 但是更可能的是内核的某个内存地址,首先减去0x80000000得到一个虚拟地址,这个虚拟地址经过页表转换得到物理地址, 由于内核的页表和物理内存直接对应,所以我们几乎可以确定那个虚拟地址就是物理地址, 所以我们才说内核地址减去0x8000_0000就是物理地址。 /***********************************************************************************************/ 不知道是不是这样子,但是根据我对386的经验,肯定是任何地址都要经过mmu管理的,内核(理应)不能绕过这个机制(因为这是硬件啊) 我觉得内核学习的过程到处都是坑,错误的概念,他们讲高端内存的时候,又不讲分页了,我觉得必须把这些东西给连起来,才能看到大局,看到大局,才能知道来龙去脉,才学的更踏实。
目录 第一章 Linux底层分段分页机制 5 1.1 基于x86的Linux分段机制 5 1.2 基于x86的Linux分页机制 7 1.2.1 页全局目录和页表 8 1.2.2 线性地址到物理地址 10 1.2.3 线性地址字段处理 13 1.2.4 页表处理 15 1.3 扩展分页与联想存储器 20 1.4 Linux内存布局 21 1.5 内核空间和用户空间 23 1.5.1 初始化临时内核页表 24 1.5.2 永久内核页表的初始化 32 1.5.3 第一次进入用户空间 41 1.5.4 内核映射机制实例 44 1.6 固定映射的线性地址 48 1.7 高端内存内核映射 50 1.8.1 永久内存映射 50 1.8.2 临时内核映射 55 第二章 内核级内存管理系统 58 2.1 Linux页面管理 58 2.1.1 NUMA架构 61 2.1.2 内存管理区 62 2.2 伙伴系统算法 65 2.2.1 数据结构 66 2.2.2 块分配 67 2.2.3 块释放 69 2.3 Linux页面级内存管理 72 2.3.1 分配一组页面 73 2.3.2 释放一组页面 80 2.4 每CPU页面高速缓存 81 2.4.1 数据结构 81 2.4.2 通过每CPU 页高速缓存分配页面 82 2.4.3 释放页面到每CPU 页面高速缓存 83 2.5 slab分配器 85 2.5.1 数据结构 86 2.5.2 分配/释放slab页面 92 2.5.3 增加slab数据结构 93 2.5.4 高速缓存内存布局 94 2.5.5 slab着色 95 2.5.6 分配slab对象 96 2.5.7 释放Slab对象 100 2.5.8 通用对象 102 2.5.9 内存池 103 2.6 非连续内存区 104 2.6.1 高端内存区回顾 105 2.6.2 非连续内存区的描述符 106 2.6.3 分配非连续内存区 109 2.6.4 释放非连续内存区 113 第三章 进程的地址空间 117 3.1 用户态内存分配 117 3.1.1 mm_struct数据结构 118 3.1.2 内核线程的内存描述符 122 3.2 线性区的数据结构 123 3.2.1 线性区数据结构 123 3.2.2 红-黑树算法 126 3.2.3 线性区访问权限 128 3.3 线性区的底层处理 130 3.3.1 查找给定地址的最邻近区 131 3.3.2 查找一个与给定的地址区间相重叠的线性区 135 3.3.3 查找一个空闲的地址区间 135 3.3.4 向内存描述符链表中插入一个线性区 137 3.4 分配线性地址区间 141 3.5 释放线性地址区间 151 3.5.1 do_munmap()函数 151 3.5.2 split_vma()函数 153 3.5.3 unmap_region()函数 155 3.6 创建和删除进程的地址空间 156 3.6.1 创建进程的地址空间 156 3.6.2 删除进程的地址空间 175 3.6.3 内核线程1号的地址空间 176 3.7 堆的管理 178 第四章 磁盘文件内存映射 182 4.1 内存映射的数据结构 182 4.2 内存映射的创建 184 4.3 内存映射的请求调页 194 4.4 刷新内存映射的脏页 203 4.5 非线性内存映射 210 第五章 页面的回收 215 5.1 页框回收概念 215 5.1.1 选择目标页 216 5.1.2 PFRA设计 217 5.2 反向映射技术 218 5.2.1 匿名页的反向映射 220 5.2.2 优先搜索树 226 5.2.3 映射页的反向映射 231 5.3 PFRA实现 235 5.3.1 最近最少使用(LRU)链表 236 5.3.2 内存紧缺回收 242 5.3.3 回收磁盘高速缓存的页 267 5.3.4 周期回收 273 5.3.5 内存不足删除程序 283 第六章 交换机制 289 6.1 交换区数据结构 289 6.1.1 创建交换区 290 6.1.2 交换区描述符 291 6.1.3 换出页标识符 293 6.2 激活和禁用交换区 295 6.2.1 sys_swapon()系统调用 296 6.2.2 sys_swapoff()系统调用 304 6.2.3 try_to_unuse()函数 308 6.3 分配和释放页槽 313 6.3.1 scan_swap_map()函数 313 6.3.2 get_swap_page()函数 316 6.3.3 swap_free()函数 318 6.4 页面的换入换出 320 6.4.1 交换高速缓存 320 6.4.2 换出页 323 6.4.3 换入页 329 第七章 缺页异常处理程序 335 7.1 总体流程 335 7.2 vma以外的错误地址 341 7.3 vma内的错误地址 346 7.3.1 handle_mm_fault()函数 348 7.3.2 请求调页 352 7.3.3 写时复制 358 7.4 处理非连续内存区访问 364

4,441

社区成员

发帖
与我相关
我的任务
社区描述
Linux/Unix社区 内核源代码研究区
社区管理员
  • 内核源代码研究区社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧