页: 主要指一组数据,可能保存在ram中也可能保存在磁盘里,主要强调一页数据;通常用固定大小(4K或4M)的页来描述逻辑地址空间
页框:限指RAM中的页,跟页大小一致;通常用页框来描述物理内存空间;
页表:页表由页表项组成,每一个页表项指向一个页框
X86提供了分段和分页的内存寻址方式,见下图:
分段逻辑地址到线性地址转换图
二级页表转换图
逻辑地址到物理地址转换图
具体参考http://blog.csdn.net/p0303230/archive/2007/12/24/1965094.aspx。
现在操作系统依靠特殊的硬件特性来禁止用户程序直接与底层硬件部分进行交互、或者禁止直接访问任意的物理地址。对于x86而言,这也是实模式和保护模式所完成的功能。
在x86架构中,逻辑地址需要经过分段、分页过程转为实际的物理地址,这是一个映射的过程,是由硬件规定的。但是,如何映射,映射规则是什么,则是软件决定的,实际上这也是linux 内核的一个重要功能。可以说映射操作由cpu和MMU共同完成,但映射规则由内核制定。
Linux 内核内存映射规则的实现主要通过建立合适的内核页表来完成。
内核代码运行在0xc00000000-0xffffffff 的地址空间范围,供1G。对内核而言,显然它应该具备访问整个物理RAM的功能,这里即存在如下问题:
内核页表的建立分为两个阶段,即临时内核页表和最终内核页表:
临时内核页表是在内核编译过程静态初始化的,这就是说内核编译时就先指定了映射规则,这样保证内核被加载后cpu就能正确找到内核代码并执行。内核引导加载程序将内核代码加载到0×100000(1MB)处(前1MB的部分物理内存通常被BIOS使用,为避免把内核装入一组不连续的页框里,linux干脆将内核加载到1MB后)。通常编译后内核是小于7MB的,因此临时内核页表的目的就是要保证能对这前8MB的物理RAM寻址,即将0×00000000~0x007fffff的物理内存映射到线性地址0xc00000000~0xc07fffff的线性地址。为建立这前8MB的映射,需要两个页表、同时在页目录表中占用2项。
临时页全局目录存放在swapper_pg_dir变量中,内核把swapper_pg_dir所有项都填充为0,除0、1、0×300(十进制768)和0×301(十进制769)除外,后两项包含了从0xc0000000到0xc07fffff间的所有线性地址。
在前面的讨论中,一直存在一个为难题,即内核本身的地址空间只有1G(0xc00000000-0xffffffff),对于RAM大于1G时,内核如何寻址? 因此显然RAM等于1G是一个分界点;另外,对32位的机器而言,通产其线性地址空间为0~4G范围,当RAM大于4G时如何寻址(主要针对cpu物理模型支持地址扩展PAE时)? 因此4G又是另一个分界点。
Linux的实现采用固定+动态映射的方式来解决上述问题。 具体做法是,内核建立0~896MB的内核映射,即将0xc0000000-0xf0000000映射到物理内存前896MB;而对后128MB的线性地址空间(0xf000000~0xffffffff)采用动态映射方法,即根据需要映射到内存896MB以后的任意地址。具体实现机制请参考<<深入理解linux内核>>。
上图是linux内核地址空间布局图。内核地址从PAGE_OFFSET(通常定义为3G,即通常说的0-3G为用户地址空间,3G-4G为内核地址空间)开始,接下来是内核映像(.text等可执行代码区);再接下来是mem_map数组,属于内核分配的动态内存区,占用约为整个RAM的1%;接下来是隔离区;接下来就是vmalloc区域;接着又是隔离区;接着为从PKMAP_BASE到FIXADDR_START的由kmap()函数映射高端内存区;接着是从FIXADDR_START到FIXADDR_TOP是一个固定大小的线形地址空间,属于专用页面映射区;最后128K为隔离区。最后说一下隔离带的作用,主要用于监测内存越界等。
本节主要介绍页框管理和内存区管理。页框管理关注以页为单位的管理;内存区管理是运行在页框管理之上的RAM管理,但重点关注具有连续的物理地址和任意长度的内存但与序列的管理。这两个管理重点需要解决的问题分别是,页框管理重点在于解决内存外碎片问题,而内存区管理需要解决的是内存内碎片问题。
页框管理,顾名思义,就是以页为单位,kernel管理内存的一种机制。内核必须能区分哪些页框属于进程,而哪些页框属于内核代码或内核数据;以及哪些页框是空闲的等等。页框的状态信息保存在page数据结构中,page结构称之为页描述符。而所有的页描述符存放在mem_map数组中,每个页描述符长度为32字节,所以mem_map所需要的空间略小于整个RAM的1%.
受硬件历史的约束,不同地址的内存页框曾经被硬件用于一些特定目的,即不同地址内的页框可能在某个时期被某个硬件特别使用,因此,不能把所有的页框当做同一类型来处理。Linux内核采用管理区的方式来实现页框管理,即将不同类型的页框划归到不同管理区。具体存在如下三类管理区:
ZONE_DMA和ZONE_NORMAL管理区内的页框被直接映射到0xc0000000-0xf0000000的线性地址空间内,内核可以直接访问;而ZONE_HIGHMEM尽管也被映射到内核地址空间内,但内核不能直接访问。
在采用alloc_pages(gfp_mask)等请求分配页框的函数中,gfp_mask 是一组标志,包括__GFP_DMA,__GFP_HIGHMEM等,用于请求在某个特定管理区内分配页框。
内核采用伙伴系统算法作为页框的分配、释放算法。 内核应该为分配一组连续的页框而建立一种健壮、高效的分配策略。伙伴算法能有效的控制外碎片的出现。其原理如下:
把所有的空闲页面分为10个块组,每组中块的大小是2的幂次方个页面,例如,第0组中块的大小都为20(1个页面),第1组中块的大小为都为21(2个页面),第9组中块的大小都为29(512个页面)。也就是说,每一组中块的大小是相同的,且这同样大小的块形成一个链表。可以通过查看/proc/buddyinfo 来了解内核伙伴算法的信息。
内存区管理主要关注具有连续的物理地址和任意长度的内存但与序列的管理。伙伴系统采用页框作为管理单元,但如何处理小内存区的请求呢?例如几十或几百个字节。如果为了分配一小片内存而分配一个整页框显然是浪费。对于内存区的管理主要需要解决的是内碎片问题,内碎片的产生主要由于请求的大小跟分配给它的大小不匹配导致的。
内核采用slab分配器来解决上内碎片问题。
进程地址空间是一个非负整数地址的有序集合:{0,1,2….}.程序员在编写应用程序,显然需要关心它的程序所运行的机器具有多大的RAM,哪些RAM又是空闲的。实质上,对于x86 (32bit)机器来说,系统假设它独立运行在0-4G的地址空间里;即链接器将代码按照0-3G(最后1G留给内核)的地址来分配其逻辑地址,例如通常0×08048000用于.text段,这可以通过修改ld的链接脚本来设置新数值。 但这带来了一个新的问题,就是内核如何管理进程的地址空间呢? 显然内核需至少要知道如下信息:
这些都通过内核的VMA的数据结构表述某段地址空间,内核称之为线性区。线性区代表了一个范围内的连续的进程地址空间的集合,这些地址空间逻辑上具有共同的特征,例如被映射到了同一个文件等。所有的线性区通过链表连接起来,最终就构成了进程的地址空间。
每个进程拥有的线性区数量有进程本身决定的,可能很多,也可能很少。因此如何管理线性区,如何高效的增加、删除、查找线性区是内核的重要内容。当前,内核采用红黑树的结构来管理线性区。对线性区的操作,如查到、增加、删除等实质也都牵涉到红黑树的操作,这些事线性区管理最核心的部分,关于红黑树的具体内容请参考书籍《深入理解linux内核》
线性区实现的另一个重要内容是访问权限控制,例如可读、可写、可执行的等等。
在linux中,对内核申请内存和应用程序申请内存采用不同的策略:
而对用户程序而言,情况完全不同:
上述策略的核心不同就在于内存分配的时机,对内核而言是立即分配;而对用户进程而言,是推迟分配,准确说是按需分配,即真正需要时才分配。 实现按需分配的核心就是缺页异常处理。
当cpu访问的页不在RAM主存中时,就会导致缺页异常中断。在linue 内核中,对缺页中断处理的大致逻辑是:
这个过程牵涉到两个重要的技术,请求调页和写时复制。
在linux中,随着系统的运行,页框逐渐被分配,为防止内存被耗尽,系统需要“定期”运行页框回收算法。其主要原理就是将一些暂时不用的页框,例如未运行的进程占用的页框,交换到swap空间中,以回收页框;下一次使用这些页框时再从swap空间将数据拷入新的页框中。这也是为什么你的应用程序所实际需要的内存可以超过整个RAM的原因,因为你的磁盘也可以当做“RAM”来使用,存放临时数据。
Linux的页框回收算法(PFRA)不回收下列类型的页框:
这些页框不会被回收,也就是说这些页不会被交换出去,即他们是常驻RAM中,系统启动一旦分配后就常驻内存,这也是我们常说的系统占用内存。
能被回收的页框主要是用户态进程申请的页框和内核高速缓存。
对于一个进程而言,用户程序跟内核协作共同完成某项功能。只不过内核是系统启动加载后常驻内存,用户程序通过系统调用(int 0×80)来执行内核代码;而且内核代码是被多个进程共享使用的,这也是处处要求内核代码必须是可重入的原因。
Linux的分段分页机制,http://blog.csdn.net/p0303230/archive/2007/12/24/1965094.aspx
内存观点,http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=290225&page=&view=&sb=&o=&vc=1
《深入理解linux内核》
《linux设备驱动程序》
《深入理解计算机系统》
《深入了解Linux虚拟内存管理》
这篇文章也算纪念我的大学生活吧,曾记得大三那年我整日逃课,躲在图书馆看《linux内核源代码情景分析》,看懂了多少如今也早已忘了,只记得我整整几个月基本都在图书馆度过;那时的我总以为内核是最高深的,所以要努力学习。 大四的毕业设计也是关于嵌入式设备驱动的,而且还在一家公司实习做了一个实际应用的设备驱动,前后性能提高很大,为此还被公司老总特别表扬过,在那对一个实习生而言已属相当难得了。 后来的生活却是世事难料,研究生经历了很多故事;毕业后也没有找到内核相关的工作,很多公司给出的理由是内核开发需要有经验的人,尤其是我当时孤身一人从南方一所学校来到北京,而我们学校在这里没有了竞争力,尽管在南方还算受欢迎。
如今的我对内核扔怀抱敬畏之心,但不再膜拜了。工程更多的是改变我们当下的生活,而真正的改变未来、改变世界的是研究,是认识哪些枯燥数字后面的规律。
联系客服