IT技术博客大学习 共学习 共进步

深入理解Linux内存管理机制(一)

淘宝网综合业务平台团队博客 2012-08-05 22:50:55 浏览 4,885 次

深入理解Linux内存管理机制(一)通过本文,您即可以:

1. 存储器硬件结构

2.分段以及对应的组织方式

3.分页以及对应的组织方式

注1:本文以Linux内核2.6.32.59本版为例,其对应的代码可以在http://www.kernel.org/pub/linux/kernel/v2.6/longterm/v2.6.32/linux-2.6.32.59.tar.bz2找到。

注2:本文所有的英文专有名词都是我随便翻译的,请对照英文原文进行理解。

注3:推荐使用Source Insight进行源码分析。

内存组织

计算机内存属于随机存储器(RAM),目前PC机广泛使用的是DDR

SDRAM,即“双倍速率同步动态随机存储器”,其本质上仍然是由n bits*m KB个内存芯片组成的,比如如果我们需要8位64KB的内存,则我们就需要2*8=16块4bits*8KB的内存块。由于计算机通常是以字节(Byte)进行数据交换的,所以对内存的地址编码一般使用字节,如上我们有64KB内存,则其地址编码为0×0000~0xFFFF,称为物理地址。对于32位机来说,由于其“地址寄存器(AR)”是32位,也就限制了其内存的最大寻址范围是2^32=4GB。

Linux将物理地址按4KB的大小划分成“帧(Frame)”。为什么是4KB?因为每一个帧都需要用一个C结构体来描述,称之为“帧描述单元(Frame Discriptor)”,如果太小,帧描述单元显然太多了,如果太大,那么在内存分配时又会造成“内碎片(Inner

Fragments)”。早些时候,计算机的内存址都是直接映射的,由于程序里的地址是写死的,这就意味着每段程序每次都只能映射对应的地址空间。这无论对程序设计者与系统都是相当大的负担。Linux使用“分段”加“分页”来解决此问题。由于它们的存在,内存地址进入了逻辑地址时代。Linux有三种地址:逻辑地址(Logic

Address)、线性地址(Linear Address)与物理地址(Physics Address)。其关系如下:

\"\"

另外,Linux支持众多CPU架构,这里只研究X86的,对应的源代码为:…/X86/… 路径。

Linux中的分段

Linux并不使用太多的分段,原因是某些RISC机器对分段的支持不好。为此Linux的分段都存在“全局描述表(GDT)”中,GDT是一个全局desc_struct数组(位于linux-2.6.32.59\\arch\\x86\\include\\asm),其结构如下:

  1. #define GDT_ENTRIES 16  
  2.   
  3. struct desc_struct gdt[GDT_ENTRIES];  
  4.   
  5. struct desc_struct {  
  6.     union {  
  7.         struct {  
  8.             unsigned int a;  
  9.             unsigned int b;  
  10.         };  
  11.         struct {  
  12.             u16 limit0; // 段大小  
  13.             u16 base0; // 段起始位置  
  14.             unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; // type表示段类型,占4位;dpl指的段运行权限,占2位  
  15.             unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; //d 表示内存地址位宽,占1位  
  16.         };  
  17.     };  
  18. } __attribute__((packed));  

所以我们可以看出,段描述结构体占8个字节,至于里面的a,b,那是老的方式,后来使用C++ Struts的Bit Fields后更方便了。type类型由以下几种:

  1. enum {  
  2.     DESC_TSS = 0×9,  
  3.     DESC_LDT = 0×2,  
  4.     DESCTYPE_S = 0×10,  /* !system */  
  5. };  

Linux主要使用以下几种段:

  • 内核代码段(Kernel Code Segment):type=10,dpl=0
  • 内核数据段(Kernel Data Segment):type=2,dpl=0
  • 用户代码段(User Code Segment):type=10,dpl=3
  • 用户数据段(User Data Segment):type=2,dpl=3
  • 任务状态段(Task State Segment),每进程一个:type=9,dpl=3

其它类型可以参见linux-2.6.32.59\\arch\\x86\\include\\asm\\segment.h,里面有非常详细的说明。

它们都存储在“全局描述符表(GDT)”。Linux本身并不使用“局部描述符表(LDT)”,当一个进程被创建时,其指向的是一个默认的LDT,不过系统并不阻止进程创建它。也就是说一个进程最多两个段描述符:TSS与LDT。由于Segment Selector为16位(为什么只有16位,这个就是历史原因了,由于X86在Real Mode下段地址只有20位,其中有效的就是16位,详见:x86

memory segmentation,但Linux段内偏移地址高达32位,所以线性地址总共是48位),其中有效的索引位仅有13位,所以GDT的最大长度为213-1=8192,除去系统保留的12个,留给进程的只有8180个入口,那么就意味Linux进程的最大数为8180/2=4090。需要注意的是,进程在创建的时候并不会马上创建自己的LDT,其指向的是GDT一个默认的LDT,里面的SD为null。只有在需要的时候进程才创建自己的LDT并把它放入GDT中。所以不管是LDT也好,TSS也好,它们都存放在GDT里面。而对于UCS与UDS,所有的进程共享一个。这样地址空间不会重复吗?不会,因为线性不是最终的物理地址,每个进程还有自己的页表,所以最终映射到物理地址是不同的。

下面我们来看看段中地址是如何转换的。假设我们需要访问内核数据段的0×00124部分,由代码知其GDT的入口为13,那么其对应的内存地址=gdtr+13*8+0×00124,假设gptr为0×02000,则最终的结果为0×02228。gdtr是一个寄存器,其为48位,用来保存GDT的第一个字节线性地址与表限。其过程如图所示:\"\"

图片来源于《Understand The Linux Kernel》

分页

相对于分段来说,分页更主流更流行一些。原因是其更灵活,其能把不同的线性地址映射到同一个物理地址上,缺点是内存必须以页大小的整数倍分配。按现在主流的4KB一页来说,如果程序只申请100B的数据,那内存浪费还是相当的大。为此,Linux使用了一种称为Slab的方法来解决这个问题,后面的文章会讲到。

因为页表本身也需要存储空间,按每页32B来算,对于4GB内存,每页4KB,共有1M页,则页表的大小为32MB,这显然不可以接受,所以后来出现了多级页表这个概念。2004年后Linux版本使用的是四级页表:第一级叫“全局目录(Page

Global Directory)“、第二级叫“页上级目录(Page

Upper Directory)”、第三级叫”页中间目录(Page

Middle Derectory)”、第四级叫”页面表(Page

Table Entry)”,最后页内偏移量“offset”,如下图:\"\"

(图片来源于:http://biancheng.dnbcw.info/linux/335152.html)

图中的cr3是一个寄存器,它存储“Global DIR”的地址。当进程切换发生时,它将被保存在TSS中,前面说过了TSS段表是每个进程一个。分页在Linux内使用的地方很多,特别是进程内的地址转换。分页有硬件支持的,特别是旁路转换缓冲(Translation

Lookaside Buffer)的出现,使用即使使用三级页表的Linux在地转转换中的实际效果也是非常好的。与段表所有的进程都共用一个的是,每个进程都拥有自己的分页。其实也正是因为所有进程都共享一个段表,每个进程才必须有自己的页表,否则相同的linear地址如何映射到不同的物理地址去?下面我们着重来研究一下Linux系统中是如何表示分页中所用到的数据结构的。

每个“帧”在Linux中都是以一个名为page(位于linux-2.6.32.59\\include\\linux\\Mm_types.h)的结构体来存储的。所有的页被放在一个类型为page名为mem_map的数组中(位于linux-2.6.32.59\\mm\\Memory.c)。代码如下(为了显示方便,仅列出部分:

  1. struct page {  
  2. unsigned long flags;          /* 帧的标志位,用枚举pageflags(位于:linux-2.6.32.59\\include\\linux\\Page-flags.h)表示,每个值的意义详见注释 */  
  3. …  
  4.     atomic_t _count;        /* 该帧被引用的数量 */  
  5.     union {  
  6.         atomic_t _mapcount; /* 所有指向该帧的页表数量*/  
  7.         …  
  8.     };  
  9.     union {  
  10.         struct {  
  11.         unsigned long private;      /*根据此页的使用情况会有不同的意义,详见源码注释*/  
  12.         …  
  13.         };  
  14. …  
  15.     };  
  16.       
  17. union {  
  18.         pgoff_t index;      /* 重要:类型即unsinged long, 指向物理帧号 */  
  19. …  
  20.     };  
  21.   
  22.   
  23.     struct list_head lru;       /* 指向最近被使用的页的双向链表,cache相关*/  
  24. };  

下面我们再来看看PGD页表。每个进程的mm_struct->pgd(位于:linux-2.6.32.59\\include\\linux\\Mm_types.h)指向自己的PGD:

  1. struct mm_struct {  
  2.         …  
  3.     pgd_t * pgd;  
  4.     …       
  5. }  

可以看出pdg实际上是一个pgd_t结构数组,pgd_t在X86系统中就是一个usinged long,其指向的就是下一级页表的地址。就这样找下去,直到找到对应的页为止,再加上页内偏移,就可以进行内存访问了。

例如线性地址为:0x91220B01,如下图,如果PGD、PUD、PMD以及PTE均5位。页内偏移12位,即页大小4KB。\"\"

那么这段内存的解析步骤是:

  1. PGD号为24,查PGD[24]得到PUD入口;
  2. PUD号为4,再查PUD[4];
  3. PMD号为36,再查PMD[36];
  4. PTE号为2,再查PTE[2];
  5. 如果最终帧地址为a:那么最后的物理地址就是a+0×0301

需要补充的是,并不是所有的内存都是使用“分页”,在内核初始化的时候,有100MB内存的样子是使用直接映射的,这是因为总是要先装入分页的初始化代码才能进行页表初始化。

总结:不知不觉也写了不少了。这次我们介绍了操作系统最基本的内存管理概念“分段”与“分页”在Linux中的实现,可以看出其与通过的概念还是很接近的。这正证明了基础知识的重要性。下一次我们将介绍Linux的内存初始化过程,如页表的建立与初始化。

——————一些资源与参考——————-

Linux SLUB 分配器详解:http://www.ibm.com/developerworks/cn/linux/l-cn-slub/

Page Frame Management:http://www.makelinux.net/books/ulk3/understandlk-CHP-8-SECT-1

Linux memory management:http://www.cse.psu.edu/~anand/spring01/linux/memory.ppt

linux内存管理浅析http://hi.baidu.com/_kouu/blog/item/f72e707ffa8478310cd7da28.html

Linux内存之页表:http://biancheng.dnbcw.info/linux/335152.html

建议继续学习

  1. MYSQL分页limit速度太慢优化方法 (阅读 5,702)
  2. Memcached内存管理机制浅析 (阅读 5,081)
  3. Mysql中的分页写法 (阅读 4,742)
  4. 独创比百度、Google分页还强的分页类 (阅读 4,702)
  5. 又一个PHP低概率Core的分析(PHP内存管理) (阅读 4,260)
  6. 合理使用MySQL的Limit进行分页 (阅读 3,921)
  7. 高效的MySQL分页 (阅读 3,761)
  8. PHP原理之内存管理中难懂的几个点 (阅读 3,641)
  9. 用Twitter的cursor方式进行Web数据分页 (阅读 3,141)
  10. 交互模式之分页还是加载? (阅读 3,001)