1 内存寻址
1.1 物理地址、虚拟地址以及线性地址
- 物理地址: 物理内存的内存单元地址
- 虚拟地址: 程序员看到的内存空间定义未虚拟地址,intel X86 CPU寻址使用了段机制,最初的8086中有4个16位的段寄存器:CS、DS、SS、ES,分别用于存放可执行代码的代码段、数据段、堆栈段和其他段的基地址,解决了CPU数据总线16位寻址20位数据地址空间的问题。 虚拟地址一般用“段:偏移量”的形式来描述,比如在8086中A815:CF2D就代表段首地址为A815,段内偏移位为CF2D的虚地址。
- 线性地址: 是指一段连续的,不分段的,范围为0到4GB的地址空间,一个线性地址就是线性地址空间的一个绝对地址。
寻址模式有2种:
-
实模式: 是 段地址+偏移量 的方式,得到物理地址;如当程序执行“mov ax,[1024]”这样一条指令时,在8086的实模式下,把某一段寄存器(比如ds)左移4位,然后与16位的偏移量(1024)相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的地址(例如ds:1024)就叫虚拟地址
-
保护模式:不 允许通过段寄存器取值得到段的起始地址,而是把虚拟地址转进一个 MMU 的硬件,经过额外的转换和检查,进而得到一个物理地址,如下图所示:
MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,支持分段机制和分页机制。分段机制把一个虚拟地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址,如下图所示:
1.2 段机制
段是虚拟地址空间的基本单位,段机制将虚拟地址空间地址转换为线性地址空间的一个线性地址。
段描述符包含下面的信息:
- 段的基地址:在线性空间段中的起始地址;
- 段的界限:在虚拟地址空间内,段内可以使用的最大偏移量;
- 段的保护属性:如段是否可以被写入或者读出、段的特权级别等等
下图说明了虚拟地址空间和线性地址空间的映射关系:
在虚拟地址转换为线性地址过程中,会做如下检查:
- 段内偏移如果大于段界限,系统讲产生异常;
- 如果对一个段进行访问,系统会根据段的保护属性检查访问者是否具有访问权限,如果没有,则产生异常。
1.3 linux中的段机制
intel段机制是从8086引入,最初是为了解决CPU内部16位地址到20位实地址的转换。为了保持兼容性,386仍然使用了段机制。linux目前所有的进程都使用了相同的逻辑空间地址,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让Linux具有更好的可移植性,linux需要去掉段机制而只使用分页机制。 然后80x86系列CPU必须使用段机制,不能绕过段机制直接给出线性地址空间地址,linux上让段的基地址为0,在32位系统下段界限为4G, 使用这种巧妙的访问绕过了段机制。
另外80X86要求必须为数据段和代码段创建不同的段,不仅如此,linux内核运行在特权级别0,和用户程序运行在特权级别3,根据80x86的段保护机制,特权级3的程序无法访问特权级别0的段,所以linux必须为内核和用户程序分别创建独立的数据段和代码段;linux内核不区分数据段和堆栈段,同时也只使用了段的2个保护级别,简化的段底层复杂的设计。
linux这样使用段机制违背了段最初的设计初衷:不同的段映射到不用的线性地址空间中。 linux上段使用了完全相同线性地址空间,他们可以相互覆盖,这样线性地址空间映射到物理地址空间,修改任何一个段的数据都会影响其他段。linux首先利用的段的特权级别保护内核段不会被用户程序访问和修改,其次引入了分页机制保护了段数据。
1.4 分页机制
分页机制再分段机制之后进行,完成线性地址到物理地址转换的过程。如果不允许分页,段机制转换的线性地址就是物理地址;如果允许分页机制,那么线性地址就需要通过分页机制找到物理地址。
线性地址空间被划分为若干块大小相等的片,称为页,并把各页编号,同样的,物理地址也被划分为若干块大小相等的片。线性地址页和物理页有映射关系,如下图所示:
那么,页的大小应该为多少?页过大或过小都会影响内存的使用率。其大小在设计硬件分页机制时就必须确定下来,例如80X86支持的标准页大小为4KB(也支持4MB)。
1.4.1 为什么使用两级分页
假设每个进程都需要占用4GB的线性地址空间,那么就需要1M个页表,每个页表需要4字节的描述信息,这样每个进程页表就得占用4M的空间,为了减少页表空间占用,使用了两级分页机制,每个进程都被分配一个页目录,只有被使用到页表才会分配到内存中。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。
1.4.2 两级页表结构
两级页表第一级成为页目录,存储在一个4K字节的页中。页目录有1K个表项,每个表项4个字节,并指向第二级表。线性地址包含一级索引、二级索引和偏移。 一级索引占线性地址的高10位,指向1k个页目录,二级索引占用中间10位,指向了物理地址的页表项,最后12位指向了物理页的偏移,如下图描述:
其中,寄存器CR3中存储了页目录的起始地址。
这里比较巧妙的地方是页都是4K的整数倍,所以低12位都是0, 利用这低12位可以存储页面的属性信息,如下所述:
- 第0位是存在位,如果P=1,表示页表地址指向的该页在内存中,如果P=0,表示不在内存中。
- 第1位是读/写位,第2位是用户/管理员位,这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护。
- 第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式
- 第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。
- 第5位是访问位,当对页目录项进行访问时,A位=1。
- 第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面。
- 第9~11位由操作系统专用,Linux也没有做特殊之用。
1.4.3 页面高速缓存
两级分页信息都是存储再内存中的,这样CPU每取一个物理数据,都必须经过至少2次内存访问,大大降低的访问速度。为了提高速度,在80X86中设置一个最近存取页的高速缓存硬件机制,它自动保持32项处理器最近使用的页表项,因此,可以覆盖128K字节的内存地址。当访问线性地址空间的某个地址时,先检查对应的页表项是否在高速缓存中,如果在,就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓存大约有90%的命中率,也就是说每次访问存储器时,只有10%的情况必须访问两级分页机构。过程如下下图所示:
1.5 linux分页机制
为了兼容32和64位系统,linux提供了3级分页机制:
- 页总目录PGD(Page Global Directory)
- 页中间目录PMD(Page Middle Directory)
- 页表PT(Page Table)
具体寻找逻辑图如下:
尽管Linux采用的是三级分页模式,但我们的讨论还是以80X86的两级分页模式为主,因此,Linux忽略中间目录层,以后,我们把页总目录就叫页目录。
每一个进程有它自己的页目录和自己的页表集。当进程切换发生时,Linux把cr3控制寄存器的内容保存在前一个执行进程的PCB中,然后把下一个要执行进程的PCB的值装入cr3寄存器中。因此,当新进程恢复在CPU上执行时,分页单元指向一组正确的页表。
2 内存管理
2.1 虚拟内存、内核空间和用户空间
linux简化了分段机制,使得虚拟地址和线性地址一致,线性地址空间在32位系统上固定为4GB大小,linux的虚拟地址空间也这么大大,启动最高1G因为内核空间,剩余的3G为用户空间,内核空间为共享,每个进程都可以通过系统调用进入内核空间。对于具体每个进程来说都拥有4GB的虚拟空间。下图给出进程虚拟空间示意图:
用户空间是进程隔离的,不用用户空间上相同的虚拟地址实际物理内存是不一样的,一个CPU同一时刻只会执行一个进程,在CPU眼中,只存在一个虚拟地址空间,进程切换的时候属于进程的页表也会发生变化,由于虚拟地址和物理地址映射主要是页表机制实现的,页表的变化也意味着物理地址空间的变化。
linux内核虽然占据了虚拟地址空间的高位,但是映射到物理内存地址空间却是从最低位开始的,示意图如下所示:
2.2 虚拟内存实现机制之间的关系
linux虚拟内存实现需要多种机制支持,核心机制如下:
- 地址映射机制
- 请页机制
- 内存回收和分配机制
- 交换机制
- 缓存和刷新机制
这几种机制关系如下图所示:
首先内核通过地址映射机制将进程虚拟地址空间映射到物理地址空间,当进程运行时,如果要使用某个页但是这个页没有建立和物理内存页的关系,就需要请页;如果有空闲内存使用,就会进行内存分配和回收,并且进行讲物理页缓存起来;如果没有足够的内存使用,就使用交换机制,腾出一部分内存;另外在地址映射中要通过TLB来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中后也要修改页表来映射文件地址。
2.3 进程用户空间管理
2.3.1 进程用户空间简述
下图是一个进程用户空间的简单划分:
进程的地址都是虚拟地址,只会为真正使用的页面映射物理内存。一个进程的用户地址空间主要由两个数据结来描述。一个是mm_struct结构,它对进程整个用户空间进行描述,简称内存描述符;另一个是vm_area_structs结构,它对用户空间中各个区间(简称虚存区进行描述,这里的虚存区就是上例中的代码区,未初始化数据区,数据区以及堆栈区等)。
把虚存区划分成一个个空间的原因是这些虚存区的来源不一样,有的来源于可执行文件映象,有的来自与共享库,而有的可能是动态分配的内存区,对不同区间可能有不同的访问权限和操作。下图简单说明了虚存区的操作:
内核内部关于进程空间管理数据结果的关系如下图所示:
2.3.2 进程用户空间创建
进程的用户空间是在执行系统调用的fork时创建的,基于写时复制的原理,子进程创建的时候继承了父进程的用户空间,仅仅是mm_struc结构的建立、vm_area_struct结构的建立以及页目录和页表的建立,并没有真正地复制一个物理页面,这也是为什么Linux内核能迅速地创建进程的原因之一。
2.3.3 虚存映射
当调用exec()系统调用开始执行一个进程时,进程的可执行映像(包括代码段、数据段等)必须装入到进程的用户地址空间。如果该进程用到了任何一个共享库,则共享库也必须装入到进程的用户空间。Linux并不将映像装入到物理内存,相反,可执行文件只是被连接到进程的用户空间中。随着进程的运行,被引用的程序部分会由操作系统装入到物理内存,这种将映像链接到进程用户空间的方法被称为“虚存映射”,也就是把文件从磁盘映射到进程的用户空间,这样把对文件的访问转化为对虚存区的访问。有两种类型的虚存映射:
- 共享的:有几个进程共享这一映射,也就是说,如果一个进程对共享的虚存区进行写,其它进程都能感觉到,而且会修改磁盘上对应的文件。
- 私有的:进程创建的这种映射只是为了读文件,而不是写文件,因此,对虚存区的写操作不会修改磁盘上的文件,由此可以看出,私有映射的效率要比共享映射的高。
除了这两种映射外,如果映射与文件无关,就叫匿名映射。
当某个可执行映像映射到进程用户空间中并开始执行时,因为只有很少一部分虚存页面装入到了物理内存,可能会遇到所访问的数据不在物理内存。这时,处理器将向Linux 报告一个页故障及其对应的故障原因,于是就用到了后面讲述的请页机制。
2.4 请页机制
当一个进程执行时,如果CPU访问到一个有效的虚地址,但是这个地址对应的页没有在内存,则CPU产生一个缺页异常。如果这个虚存区的访问权限与引起缺页异常的访问类型相匹配,则调用handle_mm_fault()函数,该函数确定如何给进程分配一个新的物理页面:
- 如果被访问的页不在内存,也就是说,这个页还没有被存放在任何一个物理页面中,那么,内核分配一个新的页面并适当地初始化;这种技术称为“请求调页”。
- 如果被访问的页在内存但是被标为只读,也就是说,它已经被存放在一个页面中,那么,内核分配一个新的页面,并把旧页面的数据拷贝到新页面来初始化它;这种技术称为“写时复制”。
2.4.1 请求调页
“请求调页”指的是一种动态内存分配技术,它把页面的分配推迟到不能再推迟为止,也就是说,一直推迟到进程要访问的页不在物理内存时为止,由此引起一个缺页异常。
请求调页技术的引入主要是因为进程开始运行时并不访问其地址空间中的全部地址;事实上,有一部分地址也许进程永远不使用。此外,程序的局部性原理保证了在程序执行的每个阶段,真正使用的进程页只有一小部分,因此临时用不着的页根本没必要调入内存。
但是,系统为此也要付出额外的开销,这是因为由请求调页所引发的每个“缺页”异常必须由内核处理,这将浪费CPU的周期。幸运的是,局部性原理保证了一旦进程开始在一组页上运行,在接下来相当长的一段时间内它会一直停留在这些页上而不去访问其它的页:这样我们就可以认为“缺页”异常是一种稀有事件。
2.4.2 写时复制
写时复制(Copy-on-write)是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如,fork()后立即调用exec(),就无需复制父进程的页了。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的PCB。这种优化可以避免拷贝大量根本就不会使用的数据
2.5 物理页的分配和回收
从虚拟内存的角度来看,页就是最小单位。体系结构不同,支持的页大小也不尽相同。有些体系结构甚至支持几种不同的页大小。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。这就意味着,在支持4KB页大小并有1GB物理内存的机器上,物理内存会被划分为262144个页。内核用struct page结构表示系统中的每个物理页,也叫页描述符。
页描述符中比较重要的域包括:
- flag域,用来存放页面的状态,这些状态包括页是不是脏的,是不是被锁定在内存中等等;
- _count域, 存放页的引用计数—也就是这一页被引用了多少次。当计数值变为0时,就说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它;
- virtual域, 页的虚拟地址;
- lru域, 存放的next和prev指针,指向最近最久未使用(LRU)链表中的相应结点,这个链表用于页面的回收。
必须要理解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述只是短暂的。即使页中所包含的数据继续存在,但是由于交换等原因,它们可能并不再和同一个page结构相关联。内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西。这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
随着用户程序的执行和结束,就需要不断地为其分配和释放物理页面。内核应该为分配一组连续的页面而建立一种稳定、高效的分配策略。但是,频繁地请求和释放不同大小的一组连续页面,必然导致在已分配的内存块中分散许多小块的空闲页面,即外碎片,由此带来的问题是,即使这些小块的空闲页面加起来足以满足所请求的页面,但是要分配一个大块的连续页面可能就根本无法满足。为此,Linux采用著名的伙伴(Buddy)算法来解决外碎片问题。
2.5.1 伙伴算法
Linux的伙伴算法把所有的空闲页面分为10个块链表,每个链表中的一个块含有2的幂次个页面,我们把这种块叫做“页块”或简称“块”。例如,第0个链表中块的大小都为20(1个页面),第1个链表中块的大小为都为21(2个页面),第9个链表中块的大小都为2^9(512个页面)。
假设要求分配的块其大小为128个页面。该算法先在块大小为128个页面的链表中查找,看是否有这样一个空闲块。如果有,就直接分配;如果没有,该算法会查找下一个更大的块,具体地说,就是在块大小为256个页面的链表中查找一个空闲块。如果存在这样的空闲块,内核就把这256个页面分为两等份,一份分配出去,另一份插入到块大小为128个页面的链表中。如果在块大小为256个页面的链表中也没有找到空闲页块,就继续找更大的块,即512个页面的块。如果存在这样的块,内核就从512个页面的块中分出128个页面满足请求,然后从384个页面中取出256个页面插入到块大小为256个页面的链表中。然后把剩余的128个页面插入到块大小为128个页面的链表中。如果512个页面的链表中还没有空闲块,该算法就放弃分配,并发出出错信号。
以上过程的逆过程就是块的释放过程,这也是该算法名字的来由。满足以下条件的两个块称为伙伴:
- 两个块的大小相同
- 两个块的物理地址连续
- 两个快必须是从同一个更大的块中分离出来
伙伴算法把满足以上条件的两个块合并为一个块,该算法是迭代算法,如果合并后的块还可以跟相邻的块进行合并,那么该算法就继续合并。
2.5.2 slab机制分配
Slab有solaris操纵系统引入,主要是基于以下考虑:
- 内核对内存区的分配取决于所存放数据的类型。例如,当给用户态进程分配页面时,内核调用__get_free_pages()函数,并用0填充所分配的页面。而给内核的数据结构分配页面时,事情没有这么简单,例如,要对数据结构所在的内存进行初始化、在不用时要收回它们所占用的内存。因此,Slab中引入了对象这个概念,所谓对象就是存放一组数据结构的内存区,其方法就是构造或析构函数,构造函数用于初始化数据结构所在的内存区,而析构函数收回相应的内存区。但为了便于理解,也可以把对象直接看作内核的数据结构。为了避免重复初始化对象,Slab分配模式并不丢弃已分配的对象,而是释放但把它们依然保留在内存中。当以后又要请求分配同一对象时,就可以从内存获取而不用进行初始化。
Linux中对Slab分配模式有所改进,它对内存区的处理并不需要进行初始化或回收。出于效率的考虑,Linux并不调用对象的构造或析构函数,而是把指向这两个函数的指针都置为空。Linux中引入Slab的主要目的是为了减少对伙伴算法的调用次数。
实际上,内核经常反复使用某一内存区。例如,只要内核创建一个新的进程,就要为该进程相关的数据结构(PCB、打开文件对象等)分配内存区。当进程结束时,收回这些内存区。因为进程的创建和撤销非常频繁,因此,Linux的早期版本把大量的时间花费在反复分配或回收这些内存区上。从Linux2.2开始,把那些频繁使用的页面保存在高速缓存中并重新使用。
可以根据对内存区的使用频率来对它分类。对于预期频繁使用的内存区,可以创建一组特定大小的专用缓冲区进行处理,以避免内碎片的产生。对于较少使用的内存区,可以创建一组通用缓冲区来处理,即使这种处理模式产生碎片,也对整个系统的性能影响不大。
硬件高速缓存的使用,又为尽量减少对伙伴算法的调用提供了另一个理由,因为对伙伴算法的每次调用都会“弄脏”硬件高速缓存,因此,这就增加了对内存的平均访问次数。
Slab分配模式把对象分组放进缓冲区,因为缓冲区的组织和管理与硬件高速缓存的命中率密切相关,因此,Slab缓冲区并非由各个对象直接构成,而是由一连串的“大块(Slab)”构成,而每个大块中则包含了若干个同种类型的对象,这些对象或已被分配,或空闲,如图4.12所示。一般而言,对象分两种,一种是大对象,一种是小对象。所谓小对象,是指在一个页面中可以容纳下好几个对象的那种。例如,一个inode结构大约占300多个字节,因此,一个页面中可以容纳8个以上的inode结构,因此,inode结构就为小对象。Linux内核中把小于512字节的对象叫做小对象,大于512字节的对象叫做大对象。
实际上,缓冲区就是主存中的一片区域,把这片区域划分为多个块,每块就是一个Slab,每个Slab由一个或多个页面组成,每个Slab中存放的就是对象。Linux把缓冲区分为专用和通用,它们分别用于不同的目的:
- 专用缓冲区主要用于频繁使用的数据结构,如task_struct、mm_struct、vm_area_struct、 file、 dentry、 inode等
- 在内核中初始化开销不大的数据结构可以合用一个通用的缓冲区。通用缓冲区最小的为32字节,然后依次为64、128、…直至128K(即32个页面),但是,对通用缓冲区的管理又采用的是Slab方式。
2.6 交换机制
当物理内存出现不足时,Linux 内存管理子系统需要释放部分物理内存页面。这一任务由内核的交换守护进程 kswapd 完成,该内核守护进程实际是一个内核线程,它在内核初始化时启动,并周期地运行。它的任务就是保证系统中具有足够的空闲页面,从而使内存管理子系统能够有效运行。
在Linux中,交换的单位是页面而不是进程。尽管交换的单位是页面,但交换还是要付出一定的代价,尤其是时间的代价。实际上,在操作系统中,时间和空间是一对矛盾,常常需要在二者之间作出平衡,有时需要以空间换时间,有时需要以时间换空间,页面交换就是典型的以时间换空间。这里要说明的是,页面交换是不得已而为之,例如在时间要求比较紧急的实时系统中,是不宜采用页面交换机制的,因为它使程序的执行在时间上有了较大的不确定性。因此,Linux给用户提供了一种选择,可以通过命令或系统调用开启或关闭交换机制。
在页面交换中,页面置换算法是影响交换性能的关键性指标,其复杂性主要与换出有关。具体说来,必须考虑三个主要问题:
- 哪种页面要换出
- 如何在交换区中存放页面
- 如何选择被交换出的页面
请注意,我们在这里所提到的页或页面指的是其中存放的数据,因此,所谓页面的换入换出实际上是指页面中数据的换入换出。
2.6.1 哪种页面被换出
交换的最终目的是页面的回收,只有与用户空间建立了映射关系的物理页面才会被换出去,而内核空间中内核所占的页面则常驻内存。可以把用户空间中的页面按其内容和性质分为以下几种:
- 进程映像所占的页面,包括进程的代码段、数据段、堆栈段以及动态分配的“存储堆”
- 通过系统调用mmap()把文件的内容映射到用户空间
- 进程间共享内存区
对于第1种情况,进程的代码段数据段所占的内存页面可以被换入换出,但堆栈所占的页面一般不被换出,因为这样可以简化内核的设计。
对于第2种情况,这些页面所使用的交换区就是被映射的文件本身。
对于第3种情况,其页面的换入换出比较复杂。
与此相对照,映射到内核空间中的页面都不会被换出。
2.6.2 如何在交换区中存放页面
我们知道物理内存被划分为若干页面,每个页面的大小为4KB。实际上,交换区也被划分为块,每个块的大小正好等于一页,我们把交换区中的一块叫做一个页插槽(page slot),意思是说,把一个物理页面插入到一个插槽中。当进行换出时,内核尽可能把换出的页放在相邻的插槽中,从而减少在访问交换区时磁盘的寻道时间。
2.6.3 如何选择被交换出的页面
页面交换是非常复杂的,其主要内容之一就是如何选择要换出的页面,我们以循序渐进的方式来讨论页面交换策略的选择。
策略一,需要时才交换。每当缺页异常发生时,就给它分配一个物理页面。如果发现没有空闲的页面可供分配,就设法将一个或多个内存页面换出到磁盘上,从而腾出一些内存页面来。这种交换策略确实简单,但有一个明显的缺点,这是一种被动的交换策略,需要时才交换,系统势必要付出相当多的时间进行换入换出。
策略二,系统空闲时交换。与策略一相比较,这是一种积极的交换策略,也就是,在系统空闲时,预先换出一些页面而腾出一些内存页面,从而在内存中维持一定的空闲页面供应量,使得在缺页中断发生时总有空闲页面可供使用。至于换出页面的选择,一般都采用LRU算法。但是这种策略实施起来也有困难,因为并没有哪种方法能准确地预测对页面的访问,所以,完全可能发生这样的情况,即一个好久没有受到访问的页面刚刚被换出去,却又要访问它了,于是又把它换进来。在最坏的情况下,有可能整个系统的处理能力都被这样的换入/换出所影响,而根本不能进行有效的计算和操作。这种现象被称为页面的“抖动”。
策略三,换出但并不立即释放。当系统挑选出若干页面进行换出时,将相应的页面写入磁盘交换区中,并修改相应页表中页表项的内容(把present标志位置为0),但是并不立即释放,而是将其page结构留在一个缓冲(cache)队列中,使其从活跃(active)状态转为不活跃(Inactive)状态。至于这些页面的最后释放,要推迟到必要时才进行。这样,如果一个页面在释放后又立即受到访问,就可以从物理页面的缓冲队列中找到相应的页面,再次为之建立映射。由于此页面尚未释放,还保留着原来的内容,就不需要磁盘读入了。经过一段时间以后,一个不活跃的内存页面一直没有受到访问,那这个页面就需要真正被释放了。
策略四,把页面换出推迟到不能再推迟为止。实际上,策略三还有值得改进的地方。首先在换出页面时不一定要把它的内容写入磁盘。如果一个页面自从最近一次换入后并没有被写过(如代码),那么这个页面是“干净的”,就没有必要把它写入磁盘。其次,即使“脏”页面,也没有必要立即写出去,可以采用策略三。至于“干净”页面,可以一直缓冲到必要时才加以回收,因为回收一个“干净”页面花费的代价很小。
2.6.4 页面交换守护进程kswapd
为了避免在CPU忙碌的时候,也就是在缺页异常发生时,临时搜索可供换出的内存页面并加以换出,Linux内核定期地检查系统内的空闲页面数是否小于预定义的极限,一旦发现空闲页面数太少,就预先将若干页面换出,以减轻缺页异常发生时系统所承受的负担。
Kswapd的执行路线分为两部分,第一部分是发现物理页面已经短缺的情况下才进行的,目的在于预先找出若干页面,且将这些页面的映射断开,使这些物理页面从活跃状态转入不活跃状态,为页面的换出做好准备。第二部分是每次都要执行的,目的在于把已经处于不活跃状态的“脏” 页面写入交换区,使他们成为不活跃的“干净”页面继续缓冲,或进一步回收这样的页面成为空闲页面。
3 其他
3.1 Linux内存相关命令
3.1.1 free
free 执行命令显示结果如下:
total used free shared buffers cached Mem: 16470320 15687728 782592 0 220368 13893088 -/+ buffers/cache: 1574272 14896048 Swap: 16777208 295956 16481252
具体含义如下:
total | used | free | shared | buffers | cached | |
---|---|---|---|---|---|---|
Mem | 总物理内存 | 当前使用的内存(包括slab+buffers+cached) | 完全没有使用的内存 | 进程间共享的内存 | 缓存文件的元数据 | 缓存文件的具体内容 |
-/+ buffers/cache | 当前使用的内存(不包括buffers+cached,但包括slab) | 未使用和缓存的内存(free+buffers+cached) | ||||
swap | 总的交换空间 | 已使用的交换空间 | 未使用的交换空间 |
系统实际可以使用的内存之和是:
avaiable_phisical_memory = free + buffers + cached + slab
- buffer :作为buffer cache的内存,是块设备的读写缓冲区,在没有文件系统的情况下,直接对磁盘进行操作的数据会缓存到buffer cache中,例如,文件系统的元数据都会缓存到buffer cache中。
- cache:作为page cache的内存,是文件的缓存,在文件层面上的数据会缓存到page cache。文件的逻辑层需要映射到实际的物理磁盘,这种映射关系由文件系统来完成。
- slab: 内核缓存,用户快速创建内核中的对象所用。
如果 cache 的值很大,说明cache住的文件数很多。如果频繁访问到的文件都能被cache住,那么磁盘的读IO 必会非常小。
可以使用下面命令主动释放 cache占用的内存
echo 1 > /proc/sys/vm/drop_caches
可以使用下面命令主动释放flush buffer中的内容。
sync
可以使用下面命令主动释放 slab占用的内存
echo 2 > /proc/sys/vm/drop_caches
3.1.2 /proc/meminfo
/proc/meminfo是了解Linux系统内存使用状况的主要接口,我们最常用的”free”、”vmstat”等命令就是通过它获取数据的 ,/proc/meminfo所包含的信息比”free”等命令要丰富得多。
cat /proc/meminfo ,输出内容如下:
MemTotal: 510080 kB(总的内存) MemFree: 17924 kB(未使用的内存) Buffers: 4644 kB(用来给文件做缓冲内存) Cached: 35104 kB(被高速缓冲存储器(cache memory)用的内存的大小) SwapCached: 4540 kB(交换空间) Active: 330988 kB(活跃使用中的缓冲或高速缓冲存储器页面文件的大小) Inactive: 137500 kB(不经常使用中的缓冲或高速缓冲存储器页面文件的大小,可能被用于其他途径) Active(anon): 318148 kB Inactive(anon): 111000 kB Active(file): 12840 kB Inactive(file): 26500 kB Unevictable: 0 kB Mlocked: 0 kB HighTotal: 0 kB HighFree: 0 kB LowTotal: 510080 kB LowFree: 17924 kB SwapTotal: 1048568 kB SwapFree: 1040780 kB Dirty: 0 kB Writeback: 4 kB AnonPages: 424508 kB Mapped: 14052 kB Shmem: 420 kB Slab: 12460 kB SReclaimable: 4536 kB SUnreclaim: 7924 kB KernelStack: 1296 kB PageTables: 4720 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 1303608 kB Committed_AS: 1058568 kB VmallocTotal: 505848 kB VmallocUsed: 1236 kB VmallocChunk: 503196 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB DirectMap4k: 6144 kB DirectMap2M: 518144 kB
3.1.3 vmstat
vmstat是一个查看虚拟内存(Virtual Memory)使用状况的工具。用法如下描述:
vmstat [-a] [-n] [-S unit] [delay [ count]] vmstat [-s] [-n] [-S unit] vmstat [-m] [-n] [delay [ count]] vmstat [-d] [-n] [delay [ count]] vmstat [-p disk partition] [-n] [delay [ count]] vmstat [-f] vmstat [-V] -a:显示活跃和非活跃内存 -f:显示从系统启动至今的fork数量 。 -m:显示slabinfo -n:只在开始时显示一次各字段名称。 -s:显示内存相关统计信息及多种系统活动数量。 delay:刷新时间间隔。如果不指定,只显示一条结果。 count:刷新次数。如果不指定刷新次数,但指定了刷新时间间隔,这时刷新次数为无穷。 -d:显示磁盘相关统计信息。 -p:显示指定磁盘分区统计信息 -S:使用指定单位显示。参数有 k 、K 、m 、M ,分别代表1000、1024、1000000、1048576字节(byte)。默认单位为K(1024 bytes) -V:显示vmstat版本信息。
下图显示一个用法实例:
字段说明:
Procs r: The number of processes waiting for run time. b: The number of processes in uninterruptible sleep. Memory swpd: the amount of virtual memory used. free: the amount of idle memory. buff: the amount of memory used as buffers. cache: the amount of memory used as cache. inact: the amount of inactive memory. (-a option) active: the amount of active memory. (-a option) Swap si: Amount of memory swapped in from disk (/s). so: Amount of memory swapped to disk (/s). IO bi: Blocks received from a block device (blocks/s). bo: Blocks sent to a block device (blocks/s). System in: The number of interrupts per second, including the clock. cs: The number of context switches per second. CPU These are percentages of total CPU time. us: Time spent running non-kernel code. (user time, including nice time) sy: Time spent running kernel code. (system time) id: Time spent idle. Prior to Linux 2.5.41, this includes IO-wait time. wa: Time spent waiting for IO. Prior to Linux 2.5.41, shown as zero.
3.1.4 top
top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,包括整体和单个进程。
top命令执行结果示例如下:
第一行是任务队列信息,同 uptime 命令的执行结果。其内容如下:
字段 | 说明 |
---|---|
09:44:34 | 当前时间 |
up 58 days,22:26 | 系统运行时间 |
1 user | 当前登录用户数 |
load average: 2.02, 2.07, 2.04 | 系统负载,即任务队列的平均长度。三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。 |
第二、三行为进程和CPU的信息
字段 | 说明 |
---|---|
Tasks: 202 total | 进程总数量 |
1 running | 正在运行的进程数 |
201 sleeping | 休眠的进程数 |
0 stopped | 停止的进程数 |
0 zombie | 僵尸进程数 |
Cps(s): 3.4% us | 用户空间占用CPU百分比 |
2.5% sy | 内核空间占用cpu百分比 |
0.6% ni | 用户进程空间内改变过优先级的进程占用CPU百分比第二、三行为进程和CPU的信息 |
93.5 % id | 空闲CPU百分比 |
0.0% wa | 等待输入输出的CPU时间百分比 |
0.0% hi | 硬件CPU中断占用百分比 |
0.1% si | 软中断占用百分比 |
最后两行为内存信息,和free类似,不在做说明。
统计信息区域的下方显示了各个进程的详细信息
序号 列名 含义 a PID 进程id b PPID 父进程id c RUSER Real user name d UID 进程所有者的用户id e USER 进程所有者的用户名 f GROUP 进程所有者的组名 g TTY 启动进程的终端名。不是从终端启动的进程则显示为 ? h PR 优先级 i NI nice值。负值表示高优先级,正值表示低优先级 j P 最后使用的CPU,仅在多CPU环境下有意义 k %CPU 上次更新到现在的CPU时间占用百分比 l TIME 进程使用的CPU时间总计,单位秒 m TIME+ 进程使用的CPU时间总计,单位1/100秒 n %MEM 进程使用的物理内存百分比 o VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES p SWAP 进程使用的虚拟内存中,被换出的大小,单位kb。 q RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA r CODE 可执行代码占用的物理内存大小,单位kb s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb t SHR 共享内存大小,单位kb u nFLT 页面错误次数 v nDRT 最后一次写入到现在,被修改过的页面数。 w S 进程状态(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程) x COMMAND 命令名/命令行 y WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名 z Flags 任务标志,参考 sched.h
附常用操作:
top //每隔5秒显式所有进程的资源占用情况 top -d 2 //每隔2秒显式所有进程的资源占用情况 top -c //每隔5秒显式进程的资源占用情况,并显示进程的命令行参数(默认只有进程名) top -p 12345 -p 6789//每隔5秒显示pid是12345和pid是6789的两个进程的资源占用情况 top -d 2 -c -p 123456 //每隔2秒显示pid是12345的进程的资源使用情况,并显式该进程启动的命令行参数
3.1.5 pmap
pmap 用于报道进程的内存映射关系。
用法
pmap PID 或者 pmap [options] PID 在输出中它显示全部的地址,kbytes,mode还有mapping。 选项 -x extended显示扩展格式 -d device显示设备格式 -q quiet不显示header/footer行 -V 显示版本信息
扩展和设备格式区域
Address: 内存开始地址 Kbytes: 占用内存的字节数(KB) RSS: 保留内存的字节数(KB) Dirty: 脏页的字节数(包括共享和私有的)(KB) Mode: 内存的权限:read、write、execute、shared、private (写时复制) Mapping: 占用内存的文件、或[anon](分配的内存)、或[stack](堆栈) Offset: 文件偏移 Device: 设备名 (major:minor)
3.1.6 ps
名称:ps 使用权限:所有使用者 使用方式:ps [options] [--help] 说明:显示瞬间行程 (process) 的动态 参数:ps的参数非常多, 在此仅列出几个常用的参数并大略介绍含义 -A 列出所有的进程 -w 显示加宽可以显示较多的资讯 -au 显示较详细的资讯 -aux 显示所有包含其他使用者的行程 常用参数: -A 显示所有进程(等价于-e)(utility) -a 显示一个终端的所有进程,除了会话引线 -N 忽略选择。 -d 显示所有进程,但省略所有的会话引线(utility) -x 显示没有控制终端的进程,同时显示各个命令的具体路径。dx不可合用。(utility) -p pid 进程使用cpu的时间 -u uid or username 选择有效的用户id或者是用户名 -g gid or groupname 显示组的所有进程。 U username 显示该用户下的所有进程,且显示各个命令的详细路径。如:ps U zhang;(utility) -f 全部列出,通常和其他选项联用。如:ps -fa or ps -fx and so on. -l 长格式(有F,wchan,C 等字段) -j 作业格式 -o 用户自定义格式。 v 以虚拟存储器格式显示 s 以信号格式显示 -m 显示所有的线程 -H 显示进程的层次(和其它的命令合用,如:ps -Ha)(utility) e 命令之后显示环境(如:ps -d e; ps -a e)(utility) h 不显示第一行
ps aux 输出格式如下:
ps -T -p pid 可以显示进程pid下面说有的线程列表, 如下范例:
3.2 overcommit
Memory Overcommit的意思是操作系统承诺给进程的内存大小超过了实际可用的内存。一个保守的操作系统不会允许memory overcommit,有多少就分配多少,再申请就没有了,这其实有些浪费内存,因为进程实际使用到的内存往往比申请的内存要少,比如某个进程malloc()了200MB内存,但实际上只用到了100MB,按照UNIX/Linux的算法,物理内存页的分配发生在使用的瞬间,而不是在申请的瞬间,也就是说未用到的100MB内存根本就没有分配,这100MB内存就闲置了。下面这个概念很重要,是理解memory overcommit的关键:commit(或overcommit)针对的是内存申请,内存申请不等于内存分配,内存只在实际用到的时候才分配。
Linux是允许memory overcommit的,只要你来申请内存我就给你,寄希望于进程实际上用不到那么多内存,但万一用到那么多了呢?那就会发生类似“银行挤兑”的危机,现金(内存)不足了。Linux设计了一个OOM killer机制(OOM = out-of-memory)来处理这种危机:挑选一个进程出来杀死,以腾出部分内存,如果还不够就继续杀…也可通过设置内核参数 vm.panic_on_oom 使得发生OOM时自动重启系统。
内核参数 vm.overcommit_memory 接受三种取值
- 0 – Heuristic overcommit handling. 这是缺省值,它允许overcommit,但过于明目张胆的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存。Heuristic的意思是“试探式的”,内核利用某种算法(对该算法的详细解释请看文末)猜测你的内存申请是否合理,它认为不合理就会拒绝overcommit。
- 1 – Always overcommit. 允许overcommit,对内存申请来者不拒。
- 2 – Don’t overcommit. 禁止overcommit。
系统具体使用配置的值可以通过这个文件 /proc/sys/vm/overcommit_memory查看
内核参数vm.overcommit_ratio 只有当vm.overcommit_memory = 2的时候才会生效,内存可申请内存为
SWAP内存大小 + 物理内存 * overcommit_ratio/100
查看系统overcommit信息
grep -i commit /proc/meminfo CommitLimit: 517584 kB Committed_AS: 3306488 kB CommitLimit:最大能分配的内存(仅在vm.overcommit_memory=2时候生效),具体的值是 SWAP内存大小 + 物理内存 * overcommit_ratio / 100 Committed_AS:当前已经分配的内存大小