linux内存机制整理

经常遇到一些刚接触Linux的新手会问内存占用怎么那么多?
在Linux中经常发现空闲内存很少,似乎所有的内存都被系统占用了,表面感觉是内存不够用了,其实不然。这是Linux内存管理的一个优秀特性,在这方 面,区别于Windows的内存管理。主要特点是,无论物理内存有多大,Linux 都将其充份利用, 将一些程序调用过的硬盘数据读入内存,利用内存读写的高速特性来提高Linux系统的数据访问性能 。而Windows是只在需要内存时, 才为应用程序分配内存,并不能充分利用大容量的内存空间。换句话说,每增加一些物理内存,Linux都将能充分利用起来,发挥了硬件投资带来的好处,而 Windows只将其做为摆设,即使增加8GB甚至更大。

Linux的这一特性,主要是利用空闲的物理内存,划分出一部份空间,做为cache、buffers ,以此提高数据访问性能。

页高速缓存(cache)是Linux内核实现的一种主要磁盘缓存。它主要用来减少对磁盘的I/O操作。具体地讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理 内存的访问。

磁盘高速缓存的价值在于两个方面:第一,访问磁盘的速度要远远低于访问内存的速度,因此,从内存访问数据比从磁盘访问速度更快。第二,数据一旦被访 问,就很有可能在短期内再次被访问到。

下面来了解下Linux内存管理机制:
一 物理内存和虚拟内存
我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,这样就引出了物理内存与虚拟内存的概念。

物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为 交换空间 (Swap Space)。

作为物理内存的扩展,linux会在物理内存不足时,使用交换分区的虚拟内存,更详细的说,就是内核会将暂时不用的内存块信息写到 交换空间 ,这样以来,物理内存得到了释放,这块内存就可以用于其它目的,当需要用到原始的内容时,这些信息会被重新从交换空间读入物理内存。

Linux的内存管理采取的是分页存取机制,为了保证物理内存能得到充分的利用,内核会在适当的时候将物理内存中不经常使用的数据块自动交换到虚拟内存中,而将经常使用的信息保留到物理内存。

要深入了解linux内存运行机制,需要知道下面提到的几个方面:
  1. Linux系统会不时的进行页面交换操作,以保持尽可能多的空闲物理内存,即使并没有什么事情需要内存,Linux也会交换出暂时不用的内存页面。这可以避免等待交换所需的时间。
  2. Linux 进行页面交换是有条件的,不是所有页面在不用时都交换到虚拟内存,linux内核根据”最近最经常使用“算法,仅仅将一些不经常使用的页面文件交换到虚拟 内存,有时我们会看到这么一个现象:linux物理内存还有很多,但是交换空间也使用了很多。其实,这并不奇怪,例如,一个占用很大内存的进程运行时,需 要耗费很多内存资源,此时就会有一些不常用页面文件被交换到虚拟内存中,但后来这个占用很多内存资源的进程结束并释放了很多内存时,刚才被交换出去的页面 文件并不会自动的交换进物理内存,除非有这个必要,那么此刻系统物理内存就会空闲很多,同时交换空间也在被使用,就出现了刚才所说的现象了。关于这点,不 用担心什么,只要知道是怎么一回事就可以了。
  3. 交换空间的页面在使用时会首先被交换到物理内存,如果此时没有足够的物理内存来容纳这些页 面,它们又会被马上交换出去,如此以来,虚拟内存中可能没有足够空间来存储这些交换页面,最终会导致linux出现假死机、服务异常等问题,linux虽 然可以在一段时间内自行恢复,但是恢复后的系统已经基本不可用了。
因此,合理规划和设计Linux内存的使用,是非常重要的.

二 内存的监控
作为一名Linux系统管理员,监控内存的使用状态是非常重要的,通过监控有助于了解内存的使用状态,比如内存占用是否正常,内存是否紧缺等等,监控内存最常使用的命令有free、top等,下面是某个系统free的输出:
[root@linuxeye ~]# free
             total       used       free     shared    buffers     cached
Mem:       3894036    3473544     420492          0      72972    1332348
-/+ buffers/cache:    2068224    1825812
Swap:      4095992     906036    3189956
每个选项的含义:
第一行:
total:物理内存的总大小
used:已经使用的物理内存大小
free:空闲的物理内存大小
shared:多个进程共享的内存大小
buffers/cached:磁盘缓存的大小

第二行Mem:代表物理内存使用情况
第三行(-/+ buffers/cached):代表磁盘缓存使用状态
第四行:Swap表示交换空间内存使用状态

free命令输出的内存状态,可以通过两个角度来查看:一个是从内核的角度来看,一个是从应用层的角度来看的

从内核的角度来查看内存的状态
就是内核目前可以直接分配到,不需要额外的操作,即为上面free命令输出中第二行Mem项的值,可以看出,此系统物理内存有3894036K,空闲的内存只有420492K,也就是40M多一点,我们来做一个这样的计算:
3894036 - 3473544 = 420492
其实就是总的物理内存减去已经使用的物理内存得到的就是空闲的物理内存大小,注意这里的可用内存值420492并不包含处于buffers和cached状态的内存大小。
如果你认为这个系统空闲内存太小,那你就错了,实际上,内核完全控制着内存的使用情况,Linux会在需要内存的时候,或在系统运行逐步推进时,将buffers和cached状态的内存变为free状态的内存,以供系统使用。

从应用层的角度来看系统内存的使用状态
也就是Linux上运行的应用程序可以使用的内存大小,即free命令第三行 -/+ buffers/cached 的输出,可以看到,此系统已经使用的内存才2068224K,而空闲的内存达到1825812K,继续做这样一个计算:
420492+(72972+1332348)=1825812
通过这个等式可知,应用程序可用的物理内存值是Mem项的free值加上buffers和cached值之和,也就是说,这个free值是包括buffers和cached项大小的, 对于应用程序来说,buffers/cached占有的内存是可用的,因为buffers/cached是为了提高文件读取的性能,当应用程序需要用到内存的时候,buffers/cached会很快地被回收,以供应用程序使用

buffers与cached的异同
在Linux 操作系统中,当应用程序需要读取文件中的数据时,操作系统先分配一些内存,将数据从磁盘读入到这些内存中,然后再将数据分发给应用程序;当需要往文件中写 数据时,操作系统先分配内存接收用户数据,然后再将数据从内存写到磁盘上。然而,如果有大量数据需要从磁盘读取到内存或者由内存写入磁盘时,系统的读写性 能就变得非常低下,因为无论是从磁盘读数据,还是写数据到磁盘,都是一个很消耗时间和资源的过程,在这种情况下,Linux引入了buffers和 cached机制。

buffers与cached都是内存操作,用来保存系统曾经打开过的文件以及文件属性信息,这样当操作系统需要读取某些文件时,会首先在buffers 与cached内存区查找,如果找到,直接读出传送给应用程序,如果没有找到需要数据,才从磁盘读取,这就是操作系统的缓存机制,通过缓存,大大提高了操 作系统的性能。但buffers与cached缓冲的内容却是不同的。

buffers是用来缓冲块设备做的,它只记录文件系统的元数据(metadata)以及 tracking in-flight pages ,而 cached是用来给文件做缓冲 。更通俗一点说:buffers主要用来存放目录里面有什么内容,文件的属性以及权限等等。而cached直接用来记忆我们打开过的文件和程序。

为了验证我们的结论是否正确,可以通过vi打开一个非常大的文件,看看cached的变化,然后再次vi这个文件,感觉一下两次打开的速度有何异同,是不是第二次打开的速度明显快于第一次呢?
接着执行下面的命令:
find /* -name  *.conf
看看buffers的值是否变化,然后重复执行find命令,看看两次显示速度有何不同。

Linux操作系统的内存运行原理,很大程度上是根据服务器的需求来设计的,例如系统的缓冲机制会把经常使用到的文件和数据缓存在cached 中,linux总是在力求缓存更多的数据和信息,这样再次需要这些数据时可以直接从内存中取,而不需要有一个漫长的磁盘操作,这种设计思路提高了系统的整 体性能。

通过本文,你可以了解: 1. 存储器硬件结构; 2.分段以及对应的组织方式; 3.分页以及对应的组织方式。 注1:本文以Linux内核2.6.32.59本版为例,其对应的代码可以在 http://www.kernel.org/

通过本文,你可以了解:
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)”,如果太小,帧描述单元显然太多了,如果太大,那么在内存分配时又会造成“内碎片(InnerFragments)”。早些时候,计算机的内存址都是直接映射的,由于程序里的地址是写死的,这就意味着每段程序每次都只能映射对应的地址空间。这无论对程序设计者与系统都是相当大的负担。Linux使用“分段”加“分页”来解决此问题。由于它们的存在,内存地址进入了逻辑地址时代。Linux有三种地址:逻辑地址(LogicAddress)、线性地址(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),其结构如下:

#define GDT_ENTRIES 16  
  
struct desc_struct gdt[GDT_ENTRIES];  
  
struct desc_struct {  
    union {  
        struct {  
            unsigned int a;  
            unsigned int b;  
        };  
        struct {  
            u16 limit0; // 段大小  
            u16 base0; // 段起始位置  
            unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; // type表示段类型,占4位;dpl指的段运行权限,占2位  
            unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; //d 表示内存地址位宽,占1位  
        };  
    };  
} __attribute__((packed));  
所以我们可以看出,段描述结构体占8个字节,至于里面的a,b,那是老的方式,后来使用C++ Struts的Bit Fields后更方便了。type类型由以下几种:
enum {  
    DESC_TSS = 0×9,  
    DESC_LDT = 0×2,  
    DESCTYPE_S = 0×10,  /* !system */  
};
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”,如下图:

图中的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)。代码如下(为了显示方便,仅列出部分:
struct page {  
unsigned long flags;          /* 帧的标志位,用枚举pageflags(位于:linux-2.6.32.59\include\linux\Page-flags.h)表示,每个值的意义详见注释 */  
  
    atomic_t _count;        /* 该帧被引用的数量 */  
    union {  
        atomic_t _mapcount; /* 所有指向该帧的页表数量*/  
          
    };  
    union {  
        struct {  
        unsigned long private;      /*根据此页的使用情况会有不同的意义,详见源码注释*/  
          
        };  
  
    };  
      
union {  
        pgoff_t index;      /* 重要:类型即unsinged long, 指向物理帧号 */  
  
    };  
  
  
    struct list_head lru;       /* 指向最近被使用的页的双向链表,cache相关*/  
};
下面我们再来看看PGD页表。每个进程的mm_struct->pgd(位于:linux-2.6.32.59\include\linux\Mm_types.h)指向自己的PGD:
struct mm_struct {  
          
    pgd_t * pgd;  
           
}
可以看出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的内存初始化过程,如页表的建立与初始化。


猜你喜欢

转载自blog.csdn.net/runner668/article/details/79995331