请求调页机制,只要用户态进程继续执行,他们就能获得页框,然而,请求调页没有办法强制进程释放不再使用的页框。因此,迟早所有空闲内存将被分配给进程和高速缓存,Linux内核的页面回收算法(PFRA)采取从用户进程和内核高速缓存“窃取”页框的办法不从伙伴系统的空闲块列表。
实际上,在用完所有空闲内存之前,就必须执行页框回收算法。否则,内核很可能陷入一种内存请求的僵局中,并导致系统崩溃。也就是说,要释放一个页框,内核就必须把页框的数据写入磁盘;但是,为了完成这一操作,内核却要请求另一个页框(例如,为I/O数据传送分配缓冲区首部)。因为不存在空闲页框,因此,不可能释放页框。
页框算法的目标之一就是保存最少的空闲页框并使内核可以安全地从“内存紧缺”的情形中恢复过来。
PFRA的目标就是获得页框并使之空闲。PFRA按照页框所含内容,以不同的方式处理页框。我们将他们区分成:不可回收页、可交换页、可同步页和可丢弃页:
页类型 |
说明 |
回收操作 |
不可回收页 |
空闲页(包含子伙伴系统列表中) 保留页(PG_reserved标志置位) 内核动态分配页 进程内核态堆栈页 临时锁定页(PG_locked标志置位) 内存锁定页(在先行区中且VM_LOCKED标志置位) |
不允许也无需回收 |
可回收页 |
用户太地址空间的匿名页 Tmpfs文件系统的映射页(如IPC共享内存的页)
|
将页的内容保存在交换区 |
可同步页 |
用户态地址空间的映射页 存有磁盘文件数据且在页高速缓存中的页 块设备缓冲区页 某些磁盘高速缓存的页(如索引节点高速缓存) |
必要时,与磁盘镜像同步这些页 |
可丢弃页 |
内存高速缓存中的未使用页(如slab分配器高速缓存) 目录想高速缓存的未使用页 |
无需操作 |
Linux 操作系统使用如下这两种机制检查系统内存的使用情况,从而确定可用的内存是否太少从而需要进行页面回收。
周期性的检查:这是由后台运行的守护进程 kswapd 完成的。该进程定期检查当前系统的内存使用情况,当发现系统内空闲的物理页面数目少于特定的阈值时,该进程就会发起页面回收的操作。
“内存严重不足”事件的触发:在某些情况下,比如,操作系统忽然需要通过伙伴系统为用户进程分配一大块内存,或者需要创建一个很大的缓冲区,而当时系统中的内存没有办法提供足够多的物理内存以满足这种内存请求,这时候,操作系统就必须尽快进行页面回收操作,以便释放出一些内存空间从而满足上述的内存请求。这种页面回收方式也被称作“直接页面回收”。这种回收是会阻塞当前进程的执行,直到回收了足够的内存再唤醒。因此你在例如samba拷贝众多小文件的时候,你可能会忽然卡住,就有可能是这个原因(当然还有可能是tcp的原因)。
睡眠回收,在进入suspend-to-disk状态时,内核必须释放内存。
如果操作系统在进行了内存回收操作之后仍然无法回收到足够多的页面以满足上述内存要求,那么操作系统只有最后一个选择,那就是使用 OOM( out of memory )killer,它从系统中挑选一个最合适的进程杀死它,并释放该进程所占用的所有页面。早期的内核版本是允许在proc文件系统中关闭OOM功能的,现在的内核都不允许了。在内核编译的时候还可以配置OOM时的表现状态,可以配置为panic,在OOM发生的时候kernel直接停机。这在很多系统中是比较常见的做法。
上面介绍的内存回收机制主要依赖于三个字段:pages_min,pages_low 以及 pages_high。每个内存区域( zone,一共3个,分别是DMA、Normal和high,可配置)都在其区域描述符中定义了这样三个字段,这三个字段的具体含义如下表 所示。
l pages_min:区域的预留页面数目,如果空闲物理页面的数目低于 pages_min,那么系统的压力会比较大,此时,内存区域中急需空闲的物理页面,页面回收的需求非常紧迫。
l pages_low:控制进行页面回收的最小阈值,如果空闲物理页面的数目低于 pages_low,那么操作系统内核会开始进行页面回收。
l pages_high:控制进行页面回收的最大阈值,如果空闲物理页面的数目多于 pages_high,则内存区域的状态是理想的。
这三个值都是通过在proc文件系统系统下的min_free_kbytes这一个变量设置并初始化的。这里有一点,对于嵌入式系统,这3个值的初始化算法非常不科学。适当的调整这三个值的初始化方式可以很大程度的提高内存使用效率。调整代码位于page_alloc.c中。
设计总则
1. 首先释放“无害”页,即必须线回收没有被任何进程使用的磁盘与内存高速缓存中的页;
2. 将用户态进程和所有页定为可回首页,FPRA必须能够窃得人任何用户态进程页,包括匿名页。这样,睡眠较长时间的进程将逐渐失去所有页;
3. 同时取消引用一个共享页的所有页表项的映射,就可以回收该共享页;
4. 只回收“未用”页,使用LRU算法。Linux使用每个页表项中的访问标志位,在页被访问时,该标志位自动置位;而且,页年龄由页描述符在链表(两个不同的链表之一)中的位置来表示。
因此,页框回收算法是集中启发式方法的混合:
1. 谨慎选择检查高速缓存的顺序;
2. 基于页年龄的变化排序;
区别对待不同状态的页;
PFRA的目标之一是能释放共享页框。为达到这个目地。Linux内核能够快速定为指向同一页框的所有页表项。这个过程就叫做反向映射。Linux操作系统为物理页面建立一个链表,用于指向引用了该物理页面的所有页表项。
基本思想如下图:
Linux采用“面向对象的反向映射”技术。实际上,对任何可回收的用户态页,内核保留系统中该页所在所有现行区(“对象”)的反向链接,每个线性区描述符(vm_area_struct 结构)存放一个指针指向一个内存描述符(mm_struct 结构),而该内存描述符又包含一个指针指向一个页全局目录(PGD)。因此,这些反向链接使得PFRA能够检索引用某页的所有页表项。因为线性区描述符比页描述符少,所以更新共享页的反向链接就比较省时间。
可以将一个文件的内容映射进内存。
可以为swap提供一个更快缓存层。通常位于内存中。
系列函数调用可以让内核获得用户端程序的页,向内写入东西,完成用户和内核的通信。
可以提供大于4KB的页
提供内核内存错误发现
允许内核标记一个页为有毒的,被标记的页的创造进程会被杀掉,进行消毒。
访问非法的内存地址(如访问未初始化的内存,访问已经释放的内存)是一件很危险的事情,如果在内核程序中使用了非法内存中的内容,可能会导致系统崩溃,如何发现并消灭这些潜在的风险,是在编写程序时都必须考虑的问题。在 Linux 系统中,gcc 会在编译的时候对内存未初始化的情况发出警告,但是它只能做一些静态的检查;另外如果系统安装了 Valgrind,也可以利用其提供的memcheck 来动态地对内存进行检查,但是它只能检查出一些用户态程序的问题,对工作在内核态的程序无能为力。因此,从事内核开发(如设备驱动程序)工作的时候,我们迫切需要一个能为内核程序提供动态内存检查的工具,所幸的是,在 Linux 2.6.31 的内核版本中,它提供了一个这样的内存检测功能 - Kmemcheck, 目前该功能只支持 x86 平台。
Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 /sys/kernel/debug/kmemleak中,Kmemcheck能够帮助定位大多数内存错误的上下文。
内存中有很多页内容是一样的,这些页没有必要在内存中有多份拷贝。这个机制就是让这些页用一个写保护的页取代。这个机制只合并匿名页不合并文件缓存页。(匿名页是指所有没有缓存文件的页)
不但对于访问文件可以提供用户意见,访问内存也可以。通过这个接口,用户可以告知内核接下来要对制定范围的内存进行何种操作,以方便内核针对该段内存进行优化。例如顺序或随机的等等。
支持内存热插播
支持离散内存
锁住一段内存空间以防止该内存被交换
把稀疏的页数据整理紧凑,如此可以占用更少的页。
内存不够用是永恒的话题。有的硬件系统CPU处理能力很高,但是内存不足,如此提供一个用CPU运算能力交换内存空间的方式就是必要的。这种机制叫做zbud。Zbud机制将一个内存页当成两个页来用,因为数据是被压缩的。
内存文件系统虽然是文件系统,当然也是内存管理的一部分。这部分的系统都比较新,例如zswap提供一个在内存中压缩的交换区,一般用作交换区的前端。就是交换数据首先写入zswap,当zswap空间不足时才启动写入时间的磁盘交换空间。读的时候也是首先查询zswap。
有了内存压缩的想法,就可以使用纯粹的压缩内存文件系统。这个文件系统叫做zram。也很容易理解,就是内存中的数据都是压缩存在的。自然也得提供一个申请压缩内存的接口,这个接口就是zsmalloc.
这个文件系统的应用我认为还是比较大的,绝大多数应用程序对效率要求不是特别高,但是又内存紧张。例如路由器的web页面和大部分的路由器应用。
我们知道内存管理有内存池这一种机制的。这种内存池对应的压缩形式就是zpool。
用于压缩缓存的磁盘数据,如此可以缓存更多的数据。
Backing-dev:如果你经常使用linux,在sys下浏览到每一个块设备的时候会很容易看到一个bdi目录,一般人都直接跳过了。但是这个目录既然出现在sys文件系统中,那必然有其作用。
bdi,即是backing device info的缩写,顾名思义它描述备用存储设备相关描述信息,这在内核代码里用一个结构体backing_dev_info来表示。
bdi,备用存储设备,简单点说就是能够用来存储数据的设备,而这些设备存储的数据能够保证在计算机电源关闭时也不丢失。这样说来,软盘存储设备、光驱存储设备、USB存储设备、硬盘存储设备都是所谓的备用存储设备(后面都用bdi来指示),而内存显然不是。
要理清的是磁盘设备有其驱动和设备的抽象塑聚结构,没有bdi的磁盘设备也是可以正常工作的。但是bdi为所有的磁盘设备,无论是何种提供了高层次的数据缓存功能。这个缓存层位于文件系统的下层和通用块层的上层。(见数据流图)
相对于内存来说,bdi设备(比如最常见的硬盘存储设备)的读写速度是非常慢的,因此为了提高系统整体性能,Linux系统对bdi设备的读写内容进行了缓冲,那些读写的数据会临时保存在内存里,以避免每次都直接操作bdi设备,但这就需要在一定的时机(比如每隔5秒、脏数据达到的一定的比率等)把它们同步到bdi设备,否则长久的呆在内存里容易丢失(比如机器突然宕机、重启),而进行间隔性同步工作的进程之前名叫pdflush,但后来在Kernel2.6.2x/3x对此进行了优化改进,产生有多个内核进程,bdi-default、flush-x:y等。
关于以前的pdflush不再多说,我们这里只讨论bdi-default和flush-x:y,这两个进程(事实上,flush-x:y为多个)的关系为父与子的关系,即bdi-default根据当前的状态Create或Destroy flush-x:y,x为块设备类型,y为此类设备的序号。如有两个TF卡,则分别为:flush-179:0、flush-179:1。
flush内核线程要刷进磁盘的页存储在bdi数据结构中定义的一个writeback对象,该对象是对writeback内核线程的描述,并且封装了需要处理的inode队列。在bdi数据结构中有一条work_list,该work队列维护了writeback内核线程需要处理的任务。如果该队列上没有work可以处理,那么writeback内核线程将会睡眠等待。
writeback对象封装了内核线程task以及需要处理的inode队列。当page cache/buffer cache需要刷新radix tree上的inode时,可以将该inode挂载到writeback对象的b_dirty队列上,然后唤醒writeback线程。在处理过程中,inode会被移到b_io队列上进行处理。多条链表的方式可以降低多线程之间的资源共享。
位于vfs层的一个接口定义,用于以页的形式缓存磁盘数据。我们知道bdi已经完成了相同的工作,为何还需要cleancache再来完成一遍。Cleancache是后来引进的,引进其的目的是为了虚拟化,它可以在多个虚拟机中缓存同一份数据,达到节省空间的目的。所以,单机的情况下使用此机制无意义。然而cleancache有另一个后端,就是存储缓存页的方式使用压缩,叫做zcache,这种情况下,就比bdi有优势了。然而bdi编译时暂时无法去掉,因此暂时无意义。
我们知道读取文件系统的文件是有缓存存在的。但是如何使用这个缓存用户可以提建议。这个需求是因为使用文件的人是用户程序,用户才知道他将如何使用一个文件,而例如是要对文件顺序读取的,内核对文件的缓存可能就是全面的,但如果是顺序读取的,内核对文件的缓存就是顺序的(读了后面丢了前面)。通过fadvise接口,用户就可以告诉内核这个信息了,以使得缓存系统更加高效的工作。
但理论如此,目前的实现却比较简单,例如顺序读取,目前仅仅是把该文件预读的页扩大了一倍。