linux为什么需要大页linux内核内存管理理

CPU内存管理和linux内存分页机制(转来学习)
一、基本概念
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到
最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与
地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上
虚拟内存(virtual memory)
这是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不真实的”,“假的”内存,例如,一个
0x内存地址,它并不对就物理地址上那个大数组中0x -
1那个地址元素;之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual
memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。有了这样的抽
像,一个程序,就可以使用比真实物理地址大得多的地址空间。(拆东墙,补西墙,银行也是这样子做的),甚至多个进程可以使用相同的地址。不奇怪,因为转换
后的物理地址并非相同的。
——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call
,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。打住了,这个问题再说下去,就收不住了。
&& 逻辑地址(logical address)
Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说
的连接器为A分配的0x这个地址就是逻辑地址。
——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,
表示为 [段标识符:段内偏移量],也就是说,上例中那个0x,应该表示为[A的代码段标识符:
0x],这样,才完整一些”
线性地址(linear address)或也叫虚拟地址(virtual
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
-------------------------------------------------------------
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段
式内存管理单元,先将为个逻辑地址转换成一个线性地址,再利用其页式内存管理单元,转换为最终物理地址。
这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。
2、CPU段式内存管理,逻辑地址如何转换为线性地址
一个逻辑地址由两部份组成,段标识符:
段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,最后两位涉及权限检查。
索引号,或者直接理解成数组下标——那它总要对应一个数组吧,它又是什么东东的索引呢?这个东东就是“段描述符(segment
descriptor)”,呵呵,段描述符具体地址描述了一个段(对于“段”这个字眼的理解,我是把它想像成,拿了一把刀,把虚拟内存,砍成若干的截——
段)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描
述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么东东——也就是它究竟是如何描述的,就理解段究竟有什么东东了,每一个段描
述符由8个字节组成,这些东东很复杂,虽然可以利用一个数据结构来定义它,不过,我这里只关心一样,就是Base字段,它描述了一个段的开始位置的线性地址。
Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表
(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。 好多概念,像绕口令一样。
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。 3、把Base +
offset,就是要转换的线性地址了。
还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。OK,来看看Linux怎么做的。
3、Linux的段式管理
Intel要求两次转换,这样虽说是兼容了,但是却是很冗余,呵呵,没办法,硬件要求这样做了,软件就只能照办,怎么着也得形式主义一样。另一方面,其它某些硬件平台,没有二次转换的概念,linux也需要提供一个高层抽像,来提供一个统一的界面。所以,的段式管理,事实上只是“哄骗”了一下硬件而已。
按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。这样做没有什么奇怪的,本来就是走形式嘛,像我们写年终总结一样。
可以得出重要的结论,“在linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!”
忽略了太多的细节,例如段的权限检查。呵呵。linux中,绝大部份进程并不例用LDT,除非使用Wine
,仿真Windows程序的时候。
4.CPU的页式内存管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页
(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page
[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。
另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就
要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。
&1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。
2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录
地址放到cr3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数
组),页的地址被放到页表中去了。 3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;
这个转换过程,应该说还是非常简单地。全部由硬件完成,虽然多了一道手续,但是节约了大量的内存,还是值得的。那么再简单地验证一下:
1、这样的二级模式是否仍能够表示4G的地址;页目录共有:2^10项,也就是说有这么多个页表每个目表对应了:2^10页;每个页中可寻址:2^12个
字节。还是2^32 = 4GB 2、这样的二级模式是否真的节约了空间;也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10
*4) = 8KB。哎,……怎么说呢!!! 红色错误,标注一下,后文贴中有此讨论。。。。。。
值得一提的是,虽然页目录和页表中的项,都是4个字节,32位,但是它们都只用高20位,低12位屏蔽为0——把页表的低12屏蔽为0,是很好理解的,因
为这样,它刚好和一个页面大小对应起来,大家都成整数增加。计算起来就方便多了。但是,为什么同时也要把页目录低12位屏蔽掉呢?因为按同样的道理,只要
屏蔽其低10位就可以了,不过我想,因为12&10,这样,可以让页目录和页表使用相同的数据结构,方便。
本贴只介绍一般性转换的原理,扩展分页、页的保护机制、page模式的分页这些麻烦点的东东就不啰嗦了……可以参考其它专业书籍。
5.Linux的页式内存管理
原理上来讲,linux只需要为每个进程分配好所需数据结构,放到内存中,然后在调度进程的时候,切换寄存器cr3,剩下的就交给硬件来完成了(呵呵,事实上要复杂得多,不过偶只分析最基本的流程)。
前面说了i386的二级页管理架构,不过有些CPU,还有三级,甚至四级架构,linux为
了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为:
页全局目录PGD(对应刚才的页目录)页上级目录PUD(新引进的)页中间目录PMD(也就新引进的)页表PT(对应刚才的页表)。
整个转换依据硬件转换原理,只是多了二次数组的索引罢了,那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?嗯,来看这种情况下,怎么来划分线性地址吧!从硬件的角
度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。从软件的角度,由于多引入了两部份,也就是说,共有五部份。——要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将页上级目录和页中间目录的长度设置为0就可以了。这样,操作系统见到的是五部
份,硬件还是按它死板的三部份划分,也不会出错,也就是说大家共建了和谐计算机系统。
这样,虽说是多此一举,但是考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了,这样,软件与硬件再次和谐——抽像就是强大
呀!!! 例如,一个逻辑地址已经被转换成了线性地址,0x,换成二制进,也就是:
内核对这个地址进行划分 PGD =
PUD = 0 PMD =
现在来理解linux针对硬件的花招,因为硬件根本看不到所谓PUD,PMD,所以,本质上要求PGD索引,直接就对应了PT的地址。而不是再到PUD和PMD中去查数组(虽然
它们两个在线性地址中,长度为0,2^0
=1,也就是说,它们都是有一个数组元素的数组),那么,内核如何合理安排地址呢?从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与
PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个
PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。
然后交给硬件,硬件对这个地址进行划分,看到的是:页目录 =
嗯,先根据(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接
着,再加一个offset,就是最终的物理地址了.
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。Posts - 55,
Articles - 0,
Comments - 28
格物 致知 诚意 正心 修身 齐家 治国 平天下
17:49 by tolimit, ... 阅读,
本文为原创,转载请注明:
本文章中系统我们假设为x86下的32位系统,暂且不分析64位系统的页表结构。
  linux下采用四级分页,一个线性地址会分为5个偏移量用于寻址,具体看图:
  虽然有四级,但并不是每一级都会用到,在linux中,对于硬件体系的不同可能会用到二级页表,三级页表,四级页表中的其中一个,如下:
64位系统:使用四级分页或三级分页,跟硬件有关。
未开启PAE(物理地址扩展)的32位系统:只使用二级分页,页上级目录和页中间目录里的值全为0。
开启PAE的32位系统:使用三级分页,这种情况下被排除在外的是页上级目录,也就是页上级目录中所有值都为0。
  图中有个cr3,它是一个寄存器,专门用于保存页全局目录的基地址,内核的主内核页全局目录的基地址保存在swapper_pg_dir全局变量中,但需要使用主内核页表时系统会把这个变量的值放入cr3寄存器,进程们自己的页全局目录基地址保存在自己的进程描述符的pgd中,当进程切换时,进程的页表也是需要切换的,就是把新的进程的进程描述符的pgd存入cr3中。这些目录和页表每一个都是用一个页框进行保存,比如一个进程有一个页全局目录,1024个页中间目录,1024个页表,那系统要为这个进程分配1个页框用于保存页全局目录,1024个页框用于保存页中间目录,1024个页框用于保存页表。当然,进程一般情况下是不会需要这么多页中间目录和页表的。
  实际上页全局目录、页上级目录、页中间目录、页表都是保存在一个一个页框中,我们知道常规情况下页框大小为4K(特殊情况有2MB、1GB),也就是页框的布局都是以4K倍数的地址进行排列的,要寻址一个页框,只需要20位地址就足够了。这些目录和页表中保存的都是表项,页全局目录保存的是页全局目录项,页中间目录保存的是页中间目录项,在32位系统中这些项都是32位(20位是所指页框的基地址,12位是标志位)的,在开启PAE后会变成64位,这些项保存着很多标志,我们罗列几个重要的:
Present标志:为1,所指的页在内存中,为0,不在。
所指的页框基地址:占20位。
Accessed标志:每当分页单元对相应页框进行寻址时设置。
Dirty标志:只用于页表项,每次对一个页框进行写操作时设置。
Read/Write标志:读写权限标志。
User/Supervisor标志:所指的页的特权级(进程能否访问)。
Page size标志:为1表示指的是2MB或4MB的页框。也就是页表是2MB或者4MB。
  在这些里面,最重要的或许就是所指页框基地址了,一个页中间目录项保存的页框基地址就是对应的页表的基地址,而页表项中保存的页框基地址,就是页(用于保存数据)的地址。而Present标志是用于判断是否发生缺页异常处理的标志。由于这些标志加上所指的页框基地址一共32位,一个4K的页框就能够保存1024个表项。
物理地址扩展(PAE)
  这个技术是用于X86_32位体系下的,因为32位线性地址最多能表示4GB大小的空间,而PAE技术将物理地址线扩大到36条,也就是CPU能够寻址64GB大小的物理内存。但是物理地址线扩大到36条,但是线性地址还是使用32位,这时候没办法用32位的线性地址去表示64GB大小的物理内存。实际上PAE做的就是让内核有多个&主内核页全局目录&,第一个主内核页全局目录寻址0~4GB的地址,第二个寻址5~8GB的地址,所以当寻址不同区域的地址时,只需要将不同的&主内核页全局目录&基地址存入cr3中。这些多个主内核页全局目录被称为页目录指针表(PDPT)。
  开启PAE后,32位系统寻址方式将大大改变:
二级分页会变成三级分页
表项的大小也由原来的32位变成了64位(原来是12位标志+20位页框基地址,变成12位标志+24位页框基地址,为什么是24位,因为64GB需要24位才能寻址完所有页框)。
页框大小将可选择4K或者2MB,通过修改表项中的Page size标志即可指定所指页框大小。
线性地址表示也变成如下:
内核启动后内核区域内存布局
  一般的,内核启动会被加载到内存的1MB开始处,而普通配置的内核大小一般小于3MB,也就是说,内核镜像被加载内存1MB~4MB的地方,而为什么0MB~1MB的内存内核不使用,因为这段内存一般是由BIOS使用和做一些硬件映射的。如下图:
  在里面我们值得注意的就是_end,它在代码里表明了内核镜像在内存中的结束地址,页表的初始化会先初始化未被内核使用的区域,最后再初始化内核使用的区域。
高端内存布局
  之前的文章中有简单地描述了高端内存区,在内核的虚拟地址空间的高端内存区中又分为三个区,分别是:非连续内存区、永久内核映射区、固定映射区。
非连续内存区是为系统硬件中断处理和内核模块生产空间一次性准备用的。
永久映射区是给系统底层空间分区和硬件及驱动准备的。
固定映射区是为用户配置和应用软件运行提供可用空间准备的。
  在图中,high_memory是高端内存区(ZONE_HIGHMEM)起始地址,VMALLOC是非连续内存区。
  在内核中,永久内核映射区和固定映射区大小一般都为4MB,也就是分别用一个页表可以囊括其所包含地址范围,其他都给非连续内存区使用。不过如果物理内存大小小于896MB的情况下,内核并不会生成高端内存区,只会有ZONE_DMA和ZONE_NORMAL两个区。
  我们知道,内核可使用的线性地址就只有1G大小(0xC0000000 ~ 0xFFFFFFFF),而用于ZONE_DMA和ZONE_NORMAL这两个区的映射已经花掉了896MB的线性地址空间,最后只剩下128MB用于映射高端内存,如果内存大于1G,比如2G的情况下,高端内存区大小就为1152MB,这个128MB大小的线性地址空间是完全不够直接映射高端内存的,所以对于高端内存的处理,linux并不会直接映射,而是在需要的时候才进行映射,不需要的时候就释放映射,回收线性地址。
  在初始化页表时,会对永久内核映射区和固定映射区分别进行初始化,但是都不会对他们进行映射处理,只有在需要使用时才会分配。
临时内核页表
  临时内核页全局目录是在内核编译过程中静态初始化的,临时页表是由startup_32()汇编函数进行初始化的,这个临时页表专门用于系统启动阶段,也就是系统第一个使用的页表,它只能让系统寻址0~8MB这段区间的物理内存,之后会被初始化好的完整页表代替。这个临时页表主要的工作就是让系统能够在实模式(不开启分页)和保护模式(开启分页)下都能够对内存的前8MB进行寻址。也就是将地址0xx007fffff这个区间的线性地址和0xCxC07fffff这个区间的线性地址映射到物理地址0xx007fffff。其实做法也很简单,就是将临时内核页全局目录的0x0、0x1、0x301、0x302项初始化好就行了。为什么是这几项,我们简单说明一下,在实模式下,也就是没有开启分页的情况下,线性地址0x对应的物理地址就是0x,而0xx007fffff这个区间的线性地址就包含在页全局目录的0x0和0x1项中。同理,0xCxC07fffff通过掩码获得的页全局目录项就是0x301和0x302。
  在阅读源码之前,我们必须对一些全局变量进行说明:
swapper_pg_dir:主内核页全局目录指针,cr3寄存器中保存的内核页全局目录地址就是从这个变量而来。
max_pfn:物理内存中最后一个页框号。
max_low_pfn:低端内存中最后一个页框号。
  对于页表的初始化,内核有一个优先级顺序,低端内存(物理内存中保留的前1MB) -&低端内存(内核未使用部分) -& 低端内存(内核使用部分) -& 高端内存(固定映射区) -& 高端内存(永久内核映射区)。
  首先,对于低端内存区域的页表初始化和高端内存固定映射区页表的初始化都集中在init_mem_mapping(void)函数中,这个函数在start_kernel() -& setup_arch()中:
1 void __init init_mem_mapping(void)
unsigned long
/* 设置了page_size_mask全局变量,这个变量决定了系统中有多少种页框大小(4K,2M,1G) */
/* 1G大小的页框只存在于64位系统中
* 4K大小的页框是普通的页框
* 2M大小的页框是32位内核开启了PAE后可选择页大小为2M
probe_page_size_mask();
/* max_pfn 和 max_low_pfn 都是由BIOS提供获取
13 #ifdef CONFIG_X86_64
/* 64位没有高端内存区 */
end = max_pfn && PAGE_SHIFT;
end = max_low_pfn && PAGE_SHIFT;
/* end为低端内存(ZONE_MDA和ZONE_NORMAL)的最大页框号 */
/* the ISA range is always mapped regardless of memory holes */
/* 0 ~ 1MB,一般内核启动时被安装在1MB开始处
* 这里先初始化 0 ~ 1MB的物理地址
init_memory_mapping(0, ISA_END_ADDRESS);
if (memblock_bottom_up()) {
/* 内核启动阶段使用的内存的结束地址,内核启动时一般使用物理内存 1MB ~ 4MB 的区域 */
unsigned long kernel_end = __pa_symbol(_end);
/* 先映射 内核结束地址 ~ ZONE_NORMAL结束地址 这块物理地址区域,如果是64位,则直接初始化到最后的内存页框,因为64位没有高端内存区 */
memory_map_bottom_up(kernel_end, end);
/* 再映射 1MB ~ 内核结束地址 这块物理地址区域 */
memory_map_bottom_up(ISA_END_ADDRESS, kernel_end);
memory_map_top_down(ISA_END_ADDRESS, end);
42 #ifdef CONFIG_X86_64
if (max_pfn & max_low_pfn) {
/* can we preseve max_low_pfn ?*/
max_low_pfn = max_
/* 高端内存区的固定映射区的初始化,只初始化好了页中间目录项和页表,页表项并没初始化 */
early_ioremap_page_table_range_init();
/* 将初始化好的内核页全局目录地址写入cr3寄存器 */
load_cr3(swapper_pg_dir);
/* 刷新tlb,每次修改了页表都需要刷新一下,有兴趣的可以查查为什么 */
__flush_tlb_all();
/* 检查一下是否有问题 */
early_memtest(0, max_pfn_mapped && PAGE_SHIFT);
  在这个函数的注释中写得很清楚了,我们先看init_memory_mapping()。
init_memory_mapping(0, ISA_END_ADDRESS)
1 /* 内核将start ~ end 这段物理地址映射到线性地址上,这个函数仅会映射低端内存区(ZONE_DMA和ZONE_NORMAL),线性地址0xC0000000 对应的物理地址是 0x */
2 unsigned long __init_refok init_memory_mapping(unsigned long start,
unsigned long end)
/* 用于保存内存段信息,每个段的页框大小不同,可能有4K,2M,1G三种 */
struct map_range mr[NR_RANGE_MR];
unsigned long ret = 0;
int nr_range,
pr_info("init_memory_mapping: [mem %#010lx-%#010lx]\n",
start, end - 1);
/* 清空mr */
memset(mr, 0, sizeof(mr));
* 根据start和end设置mr数组,并返回个数
nr_range = split_mem_range(mr, 0, start, end);
/* 遍历整个mr,将所有内存段的页框进行映射,就是将页框地址写入对应的页表中,返回的是最后映射的地址 */
for (i = 0; i & nr_ i++)
ret = kernel_physical_mapping_init(mr[i].start, mr[i].end,
mr[i].page_size_mask);
/* 调整页框映射的设置,和map_range类似,只是map_range是线性地址的映射数据,这里面是页框映射的数据 */
add_pfn_range_mapped(start && PAGE_SHIFT, ret && PAGE_SHIFT);
/* 返回最后映射的页框号 */
return ret && PAGE_SHIFT;
  继续看split_mem_range()函数
1 /* 这个函数会根据页的大小(4K,2M,1G)建立不同的内存段,1G大小的页框只会在64位系统下使用 */
2 static int __meminit split_mem_range(struct map_range *mr, int nr_range,
unsigned long start,
unsigned long end)
unsigned long start_pfn, end_pfn, limit_
unsigned long
/* 获取物理地址end的所在页框号 */
limit_pfn = PFN_DOWN(end);
/* head if not big page alignment ? */
/* 物理地址start所在页框,初始化阶段此值为0 */
pfn = start_pfn = PFN_DOWN(start);
/* 这一部分建立了一个页框大小为4K的内存段(mr) */
18 #ifdef CONFIG_X86_32
* PMD_SIZE保存页中间目录可映射区域的大小
* PAE禁用: 4M
* PAE激活: 2M
if (pfn == 0)
/* 如果pfn为0,也就是开始页框号是0,那结束页框号就是4M或者2M */
end_pfn = PFN_DOWN(PMD_SIZE);
/* 如果pfn不为0,以pfn开始(包括pfn),向上找到下一个是PMD_SIZE倍数的页框号 */
end_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE));
以下数值都是二进制表示
round_up(x,y):
round_up(x,y):
round_down(x,y):
round_down(x,1)
39 #else /* CONFIG_X86_64 */
/* 以pfn开始(包括pfn),向上找到下一个是PMD_SIZE倍数的页框号 */
end_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE));
/* 如果结束页框号超过了end所在页框号,那就选取end所在页框号为结束页框 */
if (end_pfn & limit_pfn)
end_pfn = limit_
/* 第一个内存段的页框大小为一个PMD_SIZE的大小,4M或者2M */
if (start_pfn & end_pfn) {
* mr[nr_range].start = start_pfn&&PAGE_SHIFT;
* mr[nr_range].end
= end_pfn&&PAGE_SHIFT;
* mr[nr_range].page_size_mask = 0;
* nr_range++;
nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);
/* pfn等于结束页框号,下个区创建时就会以这个pfn作为起始页框号*/
pfn = end_
/* 第二个区域,创建大小为2M的页框内存段,32位下2M的页框只有在PAE开启的情况下才会有,这个区不是一定会有的(有的条件是 32位系统 && PAE启动 && 开启2M大小页框) */
/* 以pfn开始(包括pfn),向上找到下一个是PMD_SIZE倍数的页框号,这里的情况结果一般都是 start_pfn = pfn */
start_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE));
63 #ifdef CONFIG_X86_32
/* X86_32位下的处理 */
/* 以limit_pfn开始(包括limit_pfn),向下找到上一个是PMD_SIZE倍数的页框号,这样就有可能有第三个段,有可能没有 */
end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE));
68 #else /* CONFIG_X86_64 */
/* X86_64位下的处理 */
/* 以pfn开始(包括pfn),向上找到下一个是PUD_SIZE倍数的页框号 */
end_pfn = round_up(pfn, PFN_DOWN(PUD_SIZE));
if (end_pfn & round_down(limit_pfn, PFN_DOWN(PMD_SIZE)))
end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE));
if (start_pfn & end_pfn) {
nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
page_size_mask & (1&&PG_LEVEL_2M));
pfn = end_
82 /* X64下会建立一个区域页框大小为1G的,32位下不会有 */
83 #ifdef CONFIG_X86_64
start_pfn = round_up(pfn, PFN_DOWN(PUD_SIZE));
end_pfn = round_down(limit_pfn, PFN_DOWN(PUD_SIZE));
if (start_pfn & end_pfn) {
* mr[nr_range].start = start_pfn&&PAGE_SHIFT;
* mr[nr_range].end
= end_pfn&&PAGE_SHIFT;
* mr[nr_range].page_size_mask = page_size_mask & ((1&&PG_LEVEL_2M)|(1&&PG_LEVEL_1G)));
* nr_range++;
nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
page_size_mask &
((1&&PG_LEVEL_2M)|(1&&PG_LEVEL_1G)));
pfn = end_
start_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE));
end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE));
if (start_pfn & end_pfn) {
nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
page_size_mask & (1&&PG_LEVEL_2M));
pfn = end_
106 #endif
/* 将剩余所有的页框作为一个新的4K大小页框的内存段 */
start_pfn =
end_pfn = limit_
nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);
/* 如果使用的是bootmem分配器的情况下会调整一下几个段的起始页框和结束页框 */
if (!after_bootmem)
adjust_range_page_size_mask(mr, nr_range);
/* 将相邻两个页框大小相等的区合并 */
for (i = 0; nr_range & 1 && i & nr_range - 1; i++) {
unsigned long old_
if (mr[i].end != mr[i+1].start ||
mr[i].page_size_mask != mr[i+1].page_size_mask)
/* 前一个区的结束页框等于后一个区的开始页框,并且区中页框大小相等的情况下,合并 */
old_start = mr[i].
memmove(&mr[i], &mr[i+1],
(nr_range - 1 - i) * sizeof(struct map_range));
mr[i--].start = old_
nr_range--;
/* 打印信息 */
for (i = 0; i & nr_ i++)
printk(KERN_DEBUG " [mem %#010lx-%#010lx] page %s\n",
mr[i].start, mr[i].end - 1,
(mr[i].page_size_mask & (1&&PG_LEVEL_1G))?"1G":(
(mr[i].page_size_mask & (1&&PG_LEVEL_2M))?"2M":"4k"));
/* 返回内存段的数量 */
return nr_
  可以看出内存段的数量跟init_mem_mapping()函数中的probe_page_size_mask()函数有很大关系,其实简单说就是将不同大小的页框分成一段一段的。
  将页框大小以大小分段后,调用了kernel_physical_mapping_init(),这个函数就是用于做映射了,它会直接修改页表达到映射目的。
1 /* 将内核的物理地址start到end映射到线性地址上,page_size_mask是页大小,分别有4K,2MB,1G三种大小
* start和end都是物理地址
4 unsigned long __init
5 kernel_physical_mapping_init(unsigned long start,
unsigned long end,
unsigned long page_size_mask)
int use_pse = page_size_mask == (1&&PG_LEVEL_2M);
unsigned long last_map_addr =
unsigned long start_pfn, end_
pgd_t *pgd_base = swapper_pg_
int pgd_idx, pmd_idx, pte_
unsigned long
unsigned pages_2m, pages_4k;
int mapping_
/* 根据start获取其对应的页框号,由于页大小为4KB,所以在地址里占用12位,其余的就是页框号了,这里就是start右移12位 */
start_pfn = start && PAGE_SHIFT;
/* 根据end获取其对应的页框号 */
end_pfn = end && PAGE_SHIFT;
/* 设置为1,表示此时是第一次迭代。在这个函数中需要进行两次迭代,这两次迭代不同的就是设置的表项属性不同 */
mapping_iter = 1;
if (!cpu_has_pse)
use_pse = 0;
32 repeat:
pages_2m = pages_4k = 0;
/* 等于start地址对应的页框号 */
pfn = start_
/* 根据页框号pfn获取此页框在页全局目录(pgd)项中的偏移量(pgd_idx),注意后面加了个PAGE_OFFSET(0xC0000000),所以这就会让线性地址0xC0000000与物理地址0x相应 */
pgd_idx = pgd_index((pfn&&PAGE_SHIFT) + PAGE_OFFSET);
/* 指向该页全局目录项,如果pfn为0 */
pgd = pgd_base + pgd_
* 这里会从pgd的第pgd_idx项向后遍历所有的页全局目录项,直到页框号pfn大于end_pfn为止
* 这里就会将start到end这段线性地址中所有页框对应的页表项都遍历了一遍
for (; pgd_idx & PTRS_PER_PGD; pgd++, pgd_idx++) {
/* 根据页全局目录项获取页中间目录所在页地址 */
pmd = one_md_table_init(pgd);
if (pfn &= end_pfn)
53 #ifdef CONFIG_X86_PAE
/* 根据页框对应的线性地址获取相应的页中间目录项pmd偏移量 */
pmd_idx = pmd_index((pfn&&PAGE_SHIFT) + PAGE_OFFSET);
pmd += pmd_
/* 在32位未开启PAE的情况下,pmd是空的 */
pmd_idx = 0;
/* PTRS_PER_PMD代表一个页中间目录有多少项,对于没有启动物理地址扩展的32系统下,其项数为1,其他情况下为512项 */
for (; pmd_idx & PTRS_PER_PMD && pfn & end_
pmd++, pmd_idx++) {
/* 获取页框号pfn对应的物理地址 */
unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET;
/* 如果使用了PSE,则页框大小会变成4MB,但是这里却是用pages_2m来保存,2MB大小的页框应该是PAE技术使用的,并不是PSE,这里不太明白,可能PAE代替了PSE */
if (use_pse) {
unsigned int addr2;
/* prot设置为PAGE_KERNEL_LARGE,这个值只有在第二次迭代时才会有效 */
pgprot_t prot = PAGE_KERNEL_LARGE;
/* init_prot是第一次迭代时会设置到对应的页表项中 */
pgprot_t init_prot =
__pgprot(PTE_IDENT_ATTR |
_PAGE_PSE);
pfn &= PMD_MASK && PAGE_SHIFT;
addr2 = (pfn + PTRS_PER_PTE-1) * PAGE_SIZE +
PAGE_OFFSET + PAGE_SIZE-1;
/* 检查地址是否处于内核启动所占用的内存区域 */
if (is_kernel_text(addr) ||
is_kernel_text(addr2))
prot = PAGE_KERNEL_LARGE_EXEC;
/* 2MB大小的页框计数器 */
pages_2m++;
/* 设置页表项为此页框,pfn_pte直接将页框号(pfn)强制转换为物理地址,而我们也知道pfn有start屏蔽页大小占用的位数得来,这里也就实现了直接映射(0xC0000000 映射到 0x) */
/* 注意第一次迭代传入的是init_prot,第二次是prot */
if (mapping_iter == 1)
set_pmd(pmd, pfn_pmd(pfn, init_prot));
set_pmd(pmd, pfn_pmd(pfn, prot));
pfn += PTRS_PER_PTE;
* 以下是建立普通大小的页表(4K)
/* 根据页中间目录项获取页表 */
pte = one_page_table_init(pmd);
/* 根据页框号获取页表项的偏移量 */
pte_ofs = pte_index((pfn&&PAGE_SHIFT) + PAGE_OFFSET);
/* 根据页表和页表项的偏移量获取到该pfn对应的页框的页表项 */
pte += pte_
for (; pte_ofs & PTRS_PER_PTE && pfn & end_
pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) {
/* 遍历此此页表中当前页表项及其之后的所有页表项 */
/* pgprot_t是一个64位(PAE开启)或32位(PAE禁止)的数据类型,表示这个页的保护标志 */
/* 这个值会在第二次迭代时设置到页框号对应的页表项中 */
pgprot_t prot = PAGE_KERNEL;
/* 初始化这个页的pgprot_t,这个是第一遍迭代时会设置到页表项中 */
pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR);
/* 如果该页框保存着系统的代码,则设置其标志PAGE_KERNEL_EXEC */
if (is_kernel_text(addr))
prot = PAGE_KERNEL_EXEC;
/* 4KB大小的页框计数器 */
pages_4k++;
if (mapping_iter == 1) {
/* 设置页表项为此页框,pfn_pte直接将页框号(pfn)强制转换为物理地址,而我们也知道pfn有start屏蔽页大小占用的位数得来,这里也就实现了直接映射(0xC0000000 映射到 0x) */
set_pte(pte, pfn_pte(pfn, init_prot));
/* last_map_addr保存最近映射的地址,就是我们刚映射完的页框地址 */
last_map_addr = (pfn && PAGE_SHIFT) + PAGE_SIZE;
/* 第二次迭代时调用到,传入prot */
set_pte(pte, pfn_pte(pfn, prot));
if (mapping_iter == 1) {
/* direct_pages_count[PG_LEVEL_2M] += pages_2m;
这里是做个统计 */
update_page_count(PG_LEVEL_2M, pages_2m);
/* direct_pages_count[PG_LEVEL_4K] += pages_4k;
这里也是做个统计 */
update_page_count(PG_LEVEL_4K, pages_4k);
/* 刷新一下tlb,内核页表改变了都要刷新一次tlb */
__flush_tlb_all();
/* 准备开始第二次迭代,第一次迭代设置到页表项中的pgprot_t为init_prot变量,第二次迭代设置到页表项中的是prot变量,它们的值是不同的 */
mapping_iter = 2;
/* 开始第二次迭代 */
/* 最后一次映射的地址(物理地址) */
return last_map_
  看完这里,应该很清楚低端内存是如何直接映射的了。
  回到init_mem_mapping()函数,上面分析的init_memory_mapping(0, ISA_END_ADDRESS)函数只映射了0MB ~ 1MB的物理内存,之后在memory_map_bottom_up()中映射低端内存区中剩余的其他物理内存,首先先映射内核结束地址 ~ ZONE_NORMAL最后一个页框所在地址,然后再映射1MB ~ 内核结束地址。如果没有高端内存的情况下,则直接一次映射1MB ~ 结束地址。这个过程结束后,所有低端内存区就已经直接映射完毕了。
  我们主要看看memory_map_bottom_up()的实现。
memory_map_bottom_up(map_start, map_end)
1 /* 将物理地址map_start ~ map_end 映射到内核区域 */
2 static void __init memory_map_bottom_up(unsigned long map_start,
unsigned long map_end)
unsigned long next, new_mapped_ram_size,
unsigned long mapped_ram_size = 0;
/* step_size need to be small so pgt_buf from BRK could cover it */
unsigned long step_size = PMD_SIZE;
start = map_
/* 开始页框号 */
min_pfn_mapped = start && PAGE_SHIFT;
while (start & map_end) {
if (map_end - start & step_size) {
/* 向上找到下一个step_size倍数的页框号 */
next = round_up(start + 1, step_size);
if (next & map_end)
next = map_
next = map_
/* 内核将 start ~ next 这段物理地址经过修正后映射到线性地址上,最后返回映射的大小 */
new_mapped_ram_size = init_range_memory_mapping(start, next);
/* 下一个setp_size倍数的页框号 */
/* 映射成功后,new_mapped_ram_size必定会大于mapped_ram_size(这个初始化是0),会将setp_size && 5,也就是下次一次会映射更多的页框 */
if (new_mapped_ram_size & mapped_ram_size)
step_size = get_new_step_size(step_size);
/* 统计已映射内存大小 */
mapped_ram_size += new_mapped_ram_
  核心在init_range_memory_mapping()中
1 /* 内核将start ~ end 这段物理地址映射到线性地址上 */
2 static unsigned long __init init_range_memory_mapping(
unsigned long r_start,
unsigned long r_end)
unsigned long start_pfn, end_
unsigned long mapped_ram_size = 0;
/* 遍历每一个结点的页框段,与memblock_region和NUMA有关,还没研究 */
for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, NULL) {
/* start_pfn, r_start, r_end中处于中间的那个数 */
u64 start = clamp_val(PFN_PHYS(start_pfn), r_start, r_end);
/* 同上 */
u64 end = clamp_val(PFN_PHYS(end_pfn), r_start, r_end);
if (start &= end)
* if it is overlapping with brk pgt, we need to
* alloc pgt buf from memblock instead.
can_use_brk_pgt = max(start, (u64)pgt_buf_end&&PAGE_SHIFT) &=
min(end, (u64)pgt_buf_top&&PAGE_SHIFT);
/* 又调用到init_memory_mapping,将start ~ end 这段物理地址映射到线性地址上 */
init_memory_mapping(start, end);
mapped_ram_size += end -
can_use_brk_pgt = true;
return mapped_ram_
  最后又是调用到init_memory_mapping函数进行页表的修改,这里就不再次说明了。
  整个memory_map_bottom_up()函数也说明完了,其实最后也是调用到init_memory_mapping()进行页表的修改,到这里,整个低端内存的页表初始化相信也没什么大的问题了。
高端内存(ZONE_HIGHMEM)固定映射区页表初始化
  固定映射区的页表初始化也是在init_mem_mapping(void)函数中进行,它的初始化是在低端内存区初始化结束之后,调用early_ioremap_page_table_range_init()进行初始化的。具体看看:
1 /* 固定映射区的初始化,只初始化好了页中间目录项和页表,页表项并没初始化 */
2 void __init early_ioremap_page_table_range_init(void)
pgd_t *pgd_base = swapper_pg_
unsigned long vaddr,
/* 固定映射区开始地址 */
vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
/* 固定映射区结束地址 */
end = (FIXADDR_TOP + PMD_SIZE - 1) & PMD_MASK;
/* 初始化内核的页全局目录中vaddr到end这个范围的线性地址 */
page_table_range_init(vaddr, end, pgd_base);
/* 重新启动一下固定映射区 */
early_ioremap_reset();
page_table_range_init(vaddr, end, pgd_base)
  核心函数,初始化对应的页表,但是页表项并不会初始化。
/* 初始化pgd_base指向的页全局目录中start到end这个范围的线性地址,整个函数结束后只是初始化好了页中间目录项对应的页表,但是页表中的页表项并没有初始化 */
static void __init
page_table_range_init(unsigned long start, unsigned long end, pgd_t *pgd_base)
int pgd_idx, pmd_
unsigned long
pte_t *pte = NULL;
/* 计算start到end这段线性地址区域所使用的页表数,见后面 */
unsigned long count = page_table_range_init_count(start, end);
void *adr = NULL;
/* 为这些页表分配连续物理页框 */
if (count)
adr = alloc_low_pages(count);
/* 找到vaddr线性地址对应的页全局目录中的偏移量 */
pgd_idx = pgd_index(vaddr);
/* 找到vaddr线性地址对应的页中间目录中的偏移量 */
pmd_idx = pmd_index(vaddr);
pgd = pgd_base + pgd_
for ( ; (pgd_idx & PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) {
/* 根据页全局目录项获取页中间目录所在页地址,见后面 */
pmd = one_md_table_init(pgd);
/* 获取页中间目录项 */
pmd = pmd + pmd_index(vaddr);
for (; (pmd_idx & PTRS_PER_PMD) && (vaddr != end);
pmd++, pmd_idx++) {
/* 初始化整个页中间目录项和页表,必要时会为不存在的页表分配页框,不过页表初始化后是空的,具体见后面 */
pte = page_table_kmap_check(one_page_table_init(pmd),
pmd, vaddr, pte, &adr);
vaddr += PMD_SIZE;
pmd_idx = 0;
/* page_table_range_init_count */
/* 计算start到end这段线性地址区域所使用的页表数 */
static unsigned long __init
page_table_range_init_count(unsigned long start, unsigned long end)
unsigned long count = 0;
#ifdef CONFIG_HIGHMEM
int pmd_idx_kmap_begin = fix_to_virt(FIX_KMAP_END) && PMD_SHIFT;
int pmd_idx_kmap_end = fix_to_virt(FIX_KMAP_BEGIN) && PMD_SHIFT;
int pgd_idx, pmd_
unsigned long
if (pmd_idx_kmap_begin == pmd_idx_kmap_end)
/* 根据线性地址vaddr,计算该地址所对应的页全局目录表项的偏移量 */
pgd_idx = pgd_index(vaddr);
/* 计算使用的页表数量 */
for ( ; (pgd_idx & PTRS_PER_PGD) && (vaddr != end); pgd_idx++) {
for (; (pmd_idx & PTRS_PER_PMD) && (vaddr != end);
pmd_idx++) {
if ((vaddr && PMD_SHIFT) &= pmd_idx_kmap_begin &&
(vaddr && PMD_SHIFT) &= pmd_idx_kmap_end)
vaddr += PMD_SIZE;
pmd_idx = 0;
/* one_md_table_init */
/* 根据页全局目录项获取第一个页中间目录所在页地址,注意页上级目录(pud)在32位和一些64位下是为空的, */
static pmd_t * __init one_md_table_init(pgd_t *pgd)
pmd_t *pmd_
#ifdef CONFIG_X86_PAE
/* 32位下开启了PAE的情况,页上级目录是空的,但是页中间目录需要存在 */
if (!(pgd_val(*pgd) & _PAGE_PRESENT)) {
/* 如果该页中间目录不存在,这里会分配一个页框用于这个页中间目录 */
pmd_table = (pmd_t *)alloc_low_page();
paravirt_alloc_pmd(&init_mm, __pa(pmd_table) && PAGE_SHIFT);
/* 设置页全局目录项的值为此新的页中间目录 */
set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));
/* 检查是否设置成功,成功的情况下pud中获取的第一个pmd应该等于pmd_table */
pud = pud_offset(pgd, 0);
BUG_ON(pmd_table != pmd_offset(pud, 0));
return pmd_
/* 获取第一个页上级目录 */
pud = pud_offset(pgd, 0);
/* 获取页上级目录中第一个页中间目录 */
pmd_table = pmd_offset(pud, 0);
return pmd_
/* page_table_kmap_check */
/* pte: 页表,页中间目录项pmd对应的页表
* pmd: 页中间目录项
* vaddr: 需要检查的线性地址
* lastpte: 上一个pte
* adr: 连续页框
static pte_t *__init page_table_kmap_check(pte_t *pte, pmd_t *pmd,
unsigned long vaddr, pte_t *lastpte,
void **adr)
#ifdef CONFIG_HIGHMEM
* Something (early fixmap) may already have put a pte
* page here, which causes the page table allocation
* to become nonlinear. Attempt to fix it, and if it
* is still nonlinear then we have to bug.
/* 获取固定映射区域开始地址在页中间目录中的偏移量 */
int pmd_idx_kmap_begin = fix_to_virt(FIX_KMAP_END) && PMD_SHIFT;
/* 获取固定映射区域结束地址在页中间目录中的偏移量 */
int pmd_idx_kmap_end = fix_to_virt(FIX_KMAP_BEGIN) && PMD_SHIFT;
if (pmd_idx_kmap_begin != pmd_idx_kmap_end
&& (vaddr && PMD_SHIFT) &= pmd_idx_kmap_begin
&& (vaddr && PMD_SHIFT) &= pmd_idx_kmap_end) {
/* 这个函数需要在释放掉bootmem分配器后使用 */
BUG_ON(after_bootmem);
newpte = *
/* 将页表复制到adr的页框中 */
for (i = 0; i & PTRS_PER_PTE; i++)
set_pte(newpte + i, pte[i]);
/* adr指向下一个页框 */
*adr = (void *)(((unsigned long)(*adr)) + PAGE_SIZE);
paravirt_alloc_pte(&init_mm, __pa(newpte) && PAGE_SHIFT);
/* 修改页中间目录项pmd让其对应的页表为newpte */
set_pmd(pmd, __pmd(__pa(newpte)|_PAGE_TABLE));
BUG_ON(newpte != pte_offset_kernel(pmd, 0));
/* 刷新tlb */
__flush_tlb_all();
/* 释放掉pte对应的页表 */
paravirt_release_pte(__pa(pte) && PAGE_SHIFT);
BUG_ON(vaddr & fix_to_virt(FIX_KMAP_BEGIN - 1)
&& vaddr & fix_to_virt(FIX_KMAP_END)
&& lastpte && lastpte + PTRS_PER_PTE != pte);
/* 返回初始化好的页表 */
  到这里整个固定映射区也初始化完成了。
高端内存永久内核映射区
  这块区域是最后初始化的,而非连续内存区是在分配过程中进行初始化的。其整个过程与固定映射区初始化类似,最后也调用了page_table_range_init()进行初始化。
  永久内核映射区初始化代码在native_pagetable_init() -& paging_init() -& pagetable_init() -& permanent_kmaps_init()中
/* 在pagetable_init中调用,pgd_base的地址是swapper_pg_dir,也就是页全局目录地址
* 这个函数初始化了高端内存区中的永久内核映射区,这个区只需要一个页表就可以概括整个区的线性地址,这个页表地址保存在 pkmap_page_table 变量中方便使用
static void __init permanent_kmaps_init(pgd_t *pgd_base)
unsigned long
/* kmap/unkmap系统调用是用来映射高端物理内存页到内核地址空间的api函数
* 他们分配的内核虚拟地址范围属于 [PKMAP_BASE,FIXADDR_START],大小是2M或4M的虚拟空间
vaddr = PKMAP_BASE;
page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);
pgd = swapper_pg_dir + pgd_index(vaddr);
pud = pud_offset(pgd, vaddr);
pmd = pmd_offset(pud, vaddr);
pte = pte_offset_kernel(pmd, vaddr);
/* pkmap_page_table保存了页表地址,之后如果用到永久内核映射区就很方便 */
pkmap_page_table =}

我要回帖

更多关于 linux内存管理机制 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信